Как встроить верификацию значков на свой сайт (без iframe и SDK)

Добавьте на сайт небольшой JavaScript-сниппет (или серверный fetch), который проверяет любой Open Badge через публичный JSON-LD-эндпоинт. Без iframe и сторонних трекеров.

Nacho Coll Автор Обновлено 9 мин чтения
Добавьте на сайт небольшой JavaScript-сниппет (или серверный fetch), который проверяет любой Open Badge через публичный JSON-LD-эндпоинт. Без iframe и сторонних трекеров.

Если вы выдаёте credentials, рано или поздно кто-нибудь спросит: «Откуда мне знать, что этот значок настоящий?» Честный ответ никогда не должен звучать как «доверяйте PNG». Open Badge v2.0 решает это публичным машиночитаемым эндпоинтом верификации, а виджет «Verified» на вашем сайте занимает около десяти строк кода — не iframe, не сторонний embed-скрипт и не SDK.

В этой статье показано, как именно вызвать эндпоинт и отобразить результат: на чистом JavaScript и на сервере в Node и Python. Также мы разберём, что на самом деле проверяет «верификация», как эндпоинт соотносится с человекочитаемыми страницами, которые badges.ninja уже даёт получателям, и какие ошибки делают виджет верификации внешне готовым, хотя он тихо ничего не доказывает.

Что на самом деле означает «верифицировано»

Assertion в Open Badge v2.0 — это JSON-LD-документ с фиксированной структурой: сама assertion (кто получил и когда), ссылка на BadgeClass (что это такое и кто задаёт критерии) и ссылка на профиль издателя (кто за это ручается). Когда вы «верифицируете» значок, на самом деле вы проверяете три отдельные вещи:

  1. Существует ли assertion и открывается ли она. ID награды настоящий, а не угаданный или подделанный.
  2. Не отозвана ли она. Издатель может отозвать значок постфактум — credential выдан по ошибке, нарушена политика, истёк срок сертификации — и корректный верификатор обязан это показать, а не просто подтвердить, что запись существует.
  3. Сходится ли цепочка. Ссылки assertion на badge и издателя действительно ведут на реальные, согласованные записи, а не на «осиротевшие» или несовпадающие ID.

Виджет верификации, который просто опрашивает URL и проверяет статус 200, полностью упускает пункт 2. Отозванная assertion по-прежнему отлично открывается — просто в payload у неё стоит флаг отзыва. Если ваш виджет проверяет лишь «удался ли запрос», отозванный credential отобразится как верифицированный, а это хуже, чем отсутствие виджета вообще.

Публичная страница для людей и API-эндпоинт

Каждая награда, выданная на https://badges.ninja, получает публичный URL, которым получатели могут делиться напрямую; для просмотра регистрация не нужна:

Публичная страница credential

Именно на эту страницу большинство получателей ссылаются из LinkedIn, резюме или подписи в письме — она создана для людей, показывает изображение значка, издателя и дату выдачи, и от вас для её работы ничего не требуется. API-эндпоинт ниже возвращает ту же самую assertion в виде структурированного JSON-LD — это то, что нужно, когда вы строите собственный интерфейс, а не просто ссылаетесь на наш.

Эндпоинт

GET https://api.badges.ninja/certify-badge/award/{guid}

Заголовок авторизации не нужен — эндпоинт намеренно публичный, ведь весь смысл Open Badges в том, что любой (работодатель, рекрутер, лицензирующий орган) может проверить credential без аккаунта. Полное описание контракта, включая точные названия полей возвращаемой assertion, есть в документации по верификации.

Передайте GUID награды — он есть либо в ваших собственных записях о выдаче, либо его можно взять из ссылки badges.ninja/awards/<guid>, которой с вами поделился получатель.

Чистый JS: запрос и отрисовка

Вот весь виджет. Без этапа сборки, без npm-пакета — просто вставьте на любую страницу:

<div id="badge-verify"></div>
<script>
  async function renderBadgeVerification(awardId, mountId) {
    const el = document.getElementById(mountId);
    el.textContent = "Checking badge status";
    try {
      const res = await fetch(
        `https://api.badges.ninja/certify-badge/award/${awardId}`
      );
      if (!res.ok) {
        el.textContent = "Credential not found.";
        return;
      }
      const assertion = await res.json();
      // —— revocation check first, a resolvable assertion isn't enough on its own
      if (assertion.revoked) {
        el.textContent = `Revoked: ${assertion.revocationReason || "no reason given"}`;
        return;
      }
      el.textContent = `Verified, issued ${new Date(assertion.issuedOn).toLocaleDateString()}`;
    } catch (err) {
      el.textContent = "Verification check failed.";
    }
  }
  renderBadgeVerification("REPLACE_WITH_AWARD_ID", "badge-verify");
</script>

Замените REPLACE_WITH_AWARD_ID на настоящий GUID награды. Вот и вся клиентская интеграция: нет границы iframe, с которой приходится бороться в CSS, и нет стороннего скрипта, который вместе с проверкой верификации подтягивает собственную аналитику или трекинговые пиксели.

На сервере: Node

Если вы предпочитаете верифицировать на бэкенде — скажем, прежде чем показать значок «Verified Partner» на подконтрольной вам странице профиля или принять credential как подтверждение в процессе онбординга, — запрос тот же, только без части про DOM:

async function verifyAward(awardId) {
  const res = await fetch(
    `https://api.badges.ninja/certify-badge/award/${awardId}`
  );
  if (!res.ok) return { verified: false, reason: "not_found" };
  const assertion = await res.json();
  if (assertion.revoked) {
    return { verified: false, reason: assertion.revocationReason || "revoked" };
  }
  return { verified: true, issuedOn: assertion.issuedOn, badge: assertion.badge };
}

На сервере: Python

import requests
# —— resolve the assertion, then check revocation before trusting it
def verify_award(award_id):
    res = requests.get(f"https://api.badges.ninja/certify-badge/award/{award_id}")
    if res.status_code != 200:
        return {"verified": False, "reason": "not_found"}
    assertion = res.json()
    if assertion.get("revoked"):
        return {"verified": False, "reason": assertion.get("revocationReason", "revoked")}
    return {"verified": True, "issued_on": assertion.get("issuedOn"), "badge": assertion.get("badge")}

Оба примера считают ответ, отличный от 200, ситуацией «не найдено», а не бросают исключение. Нерабочий или угаданный ID награды — ожидаемый ввод для публичного эндпоинта верификации, а не исключительный случай, поэтому обрабатывайте его как обычную ветку, а не как перехваченную ошибку.

Стилизуйте результат сами, а не встраивайте чужой интерфейс

Поскольку виджет выше — это просто вызов fetch с выводом в ваш собственный DOM, каждый пиксель принадлежит вам. Распространённый подход — три визуальных состояния, напрямую вытекающих из ответа:

  • Verified (верифицировано) — зелёная галочка, слово «Verified» и дата выдачи, оформленные в стиле компонентов бейджей/тегов вашего сайта.
  • Revoked (отозвано) — приглушённое или красное состояние, которое показывает причину отзыва, а не скрывает её. И получатели, и третьи лица выигрывают от честного «почему», а не от молчаливой ошибки.
  • Not found (не найдено) — визуально оформляйте так же, как «отозвано» (не показывайте ложное состояние успеха), но с другим текстом, поскольку неверный ID и намеренный отзыв — разные ситуации для того, кто читает страницу.

Для всего этого не нужна библиотека компонентов. Хватит switch по verified / revoked / not_found и трёх CSS-классов.

Кэширование, чтобы не нагружать эндпоинт

Эндпоинт верификации публичный и не требует аутентификации, а значит, за каждый запрос не списываются кредиты, — но при вызове на клиенте без кэша это всё равно сетевой запрос при каждой загрузке страницы. Если страница показывает один и тот же значок всем посетителям (например, значок «Verified Partner» в подвале), кэшируйте результат на сервере с коротким TTL — пяти–пятнадцати минут вполне достаточно, ведь отзывы — не быстро меняющийся сигнал, — вместо повторного запроса на каждый визит:

const cache = new Map();
async function verifyAwardCached(awardId, ttlMs = 10 * 60 * 1000) {
  const hit = cache.get(awardId);
  if (hit && Date.now() - hit.at < ttlMs) return hit.result;
  const result = await verifyAward(awardId);
  cache.set(awardId, { result, at: Date.now() });
  return result;
}

Для виджета «на посетителя» (получатель встраивает свой credential на личном сайте) кэш можно пропустить и просто делать свежий вызов при загрузке страницы — дополнительный запрос в таком масштабе незаметен.

Что badges.ninja уже даёт получателям

Прежде чем строить полностью свой embed, стоит узнать, что доступно из коробки. На публичной странице каждой награды есть готовые сниппеты для публикации и встраивания, которые получатели могут скопировать без какого-либо кода со своей стороны:

Публичная страница шаринга с кнопками соцсетей и embed-сниппетами

Если ваша задача — «позволить получателю показать, что его значок настоящий», ссылка на его страницу badges.ninja/awards/<guid> часто проще, чем собственный виджет, — это та самая страница для людей, показанная выше, и она уже сама различает «верифицировано» и «отозвано». Собственный виджет из этой статьи стоит строить, когда статус верификации нужно показать внутри интерфейса вашего собственного продукта — в каталоге партнёров, во внутренней панели аудита, в инструменте найма, проверяющем заявленные сертификаты кандидата, — где ссылка на другой сайт не подходит с точки зрения UX.

Почему не iframe или виджет вендора?

Некоторые платформы дают вам сниппет вроде <iframe src="https://vendor.example.com/verify-widget"> для встраивания статуса верификации. Он работает, но имеет реальные издержки: вы не можете изменить стиль содержимого под свой сайт (приходится мириться с тем CSS, который поставил вендор), он добавляет кросс-доменный документ, который ваша Content-Security-Policy должна явно разрешить, и каждая загрузка страницы отправляет запрос на хост iframe третьей стороны независимо от того, интересует ли посетителя статус верификации вообще. Fetch-виджет из двух вызовов избавляет от всех трёх проблем — это ваш DOM, ваши стили, и он запускается только тогда, когда вы решите его вызвать.

Проход по всей цепочке верификации

Если вы строите что-то более основательное, чем виджет статуса, — например, инструмент аудита, проверяющий пакет credentials, или отчёт для комплаенса, — проходите по всей цепочке, а не останавливайтесь на assertion. Assertion ссылается на URL badge (BadgeClass: название, описание, критерии), а BadgeClass, в свою очередь, ссылается на URL issuer (профиль организации). Строгий верификатор разрешает все три и подтверждает их внутреннюю согласованность — badge действительно принадлежит заявленному издателю, assertion действительно ссылается на заявленный badge, — а не просто проверяет, что верхнеуровневая assertion вернула 200. Это глубже, чем нужно виджету статуса для одного credential, но полезно знать, что такая структура существует, если партнёр или аудитор спросит: «Насколько глубоко на самом деле идёт ваша верификация?»

Типичные ошибки, которых стоит избегать

  • Проверка только HTTP-статуса. Как сказано выше, отозванная assertion всё равно открывается. Всегда читайте assertion.revoked, не выводите верификацию только из кода статуса.
  • Жёстко прописанный ID награды в общем компоненте. Если виджет предназначен для повторного использования на страницах многих получателей, принимайте ID награды как параметр (data-атрибут, query string, prop), а не как литерал, зашитый в скрипт.
  • Молчаливое проглатывание сетевых ошибок. try/catch, который ничего не логирует и показывает тот же текст «не найдено», что и для действительно отсутствующего значка, сильно осложняет отладку позже. Различайте «сетевой вызов не удался» и «API ответил отказом» в ветках ошибок — хотя бы в логах.
  • Повторная верификация при каждом рендере в клиентском приложении. Если виджет живёт внутри компонента React или Vue, который часто перерисовывается, верифицируйте один раз на монтирование (или один раз за окно TTL кэша, как в разделе о кэшировании выше), а не при каждой перерисовке.

Тратятся ли кредиты и работает ли это на бесплатном плане?

Нет. Эндпоинт верификации намеренно находится вне системы кредитов, которая регулирует выдачу: проверка валидности значка ничего не стоит издателю, а вызывающему посетителю не нужны ни аккаунт, ни API-ключ, ни какой-либо тарифный план. Это верно независимо от того, на каком плане — Free, Starter или Pro — был выдан значок: верификация — свойство самого credential, а не функция, закрытая подпиской издателя. Поэтому вы можете спокойно разместить этот виджет на публичной странице с непредсказуемым трафиком (страница вакансий, каталог партнёров), не беспокоясь о лимитах запросов, привязанных к вашему аккаунту, — вы обращаетесь к публичному эндпоинту только для чтения, а не к авторизованному маршруту API.

Стоит прямо отметить: JSON assertion, возвращаемый эндпоинтом, не содержит исходный адрес электронной почты получателя. Email получателя хранится на бэкенде в виде хеша — именно для того, чтобы публичный ответ верификации нельзя было парсить ради личных контактных данных. Вы получаете достаточно, чтобы подтвердить credential (badge, издатель, даты, статус отзыва), не превращая эндпоинт в инструмент генерации лидов для любого, кто перебирает ID наград.

Готовы выдать свой первый верифицируемый credential?

Начните бесплатно на badges.ninja — визуальный дизайнер, публичная страница верификации, PDF-сертификат, вывод в формате Open Badge v2.0. Кредитная карта не нужна.

Полную аннотированную структуру каждого поля assertion смотрите в справочнике по полям JSON-LD. А если вы выдаёте credentials программно, а не только верифицируете их, быстрый старт по API описывает аутентификацию и вызов создания награды, из которого берётся вывод этого эндпоинта.

Nacho Coll

Об авторе

Founder & Engineer at Badges Ninja

Nacho founded Badges Ninja to make issuing verifiable digital credentials as simple as a single API call — Open Badge v2.0 badges and certificates, minted, hosted, and verifiable without standing up your own issuer infrastructure. Writes about the Open Badges spec, credential verification, and running a credentialing platform serverless on AWS, from the operator side of the wire.

Назад в блог

Похожие статьи