Web サイトにバッジ検証を埋め込む(iframe も SDK も不要)

小さな JavaScript スニペット(またはサーバー側の fetch)を追加するだけで、公開 JSON-LD エンドポイントを使ってあらゆる Open Badge を検証できます。iframe もサードパーティのトラッカーも不要です。

Nacho Coll 著者: 更新日 18 分で読了
小さな JavaScript スニペット(またはサーバー側の fetch)を追加するだけで、公開 JSON-LD エンドポイントを使ってあらゆる Open Badge を検証できます。iframe もサードパーティのトラッカーも不要です。

資格情報を発行していれば、遅かれ早かれ「このバッジが本物だとどうやって分かるのですか?」と聞かれます。その答えが「PNG を信じてください」であってはなりません。Open Badge v2.0 には、公開されていて機械で読み取れる検証エンドポイントがあります。自社サイトに「Verified」ウィジェットを置くのに必要なコードは約 10 行だけです。iframe も、サードパーティの埋め込みスクリプトも、SDK も使いません。

この記事では、エンドポイントの呼び出し方と結果の表示方法を、バニラ JavaScript と、サーバー側の Node・Python で示します。あわせて、「検証」が実際に何をチェックするのか、badges.ninja が受領者向けにすでに用意している人間向けページとこのエンドポイントの関係、そして検証ウィジェットが完成して見えるのに実は何も証明していない、というありがちな間違いも取り上げます。

「検証済み」が実際に意味すること

Open Badge v2.0 の Assertion は、決まった構造を持つ JSON-LD ドキュメントです。Assertion 自体(誰がいつ受け取ったか)、BadgeClass への参照(バッジの内容と、基準を定めるのは誰か)、そして発行者プロファイルへの参照(誰が保証しているか)で構成されます。バッジを「検証」するとき、実際には 3 つの別々の点を確認しています。

  1. Assertion が存在し、解決できるか。 授与 ID が実在するもので、推測されたものでも改ざんされたものでもないこと。
  2. 失効していないか。 発行者は、誤発行、ポリシー違反、期限切れの認定などを理由に、後からバッジを失効させられます。正しい検証では、レコードが存在することを確認するだけでなく、失効も必ず表示する必要があります。
  3. チェーン全体が整合しているか。 Assertion が参照するバッジと発行者が、実在し、互いに一致するレコードに解決されること。孤立した ID や不一致の ID ではないこと。

URL に ping を送って 200 ステータスだけを確認するウィジェットでは、2 点目をまったく見逃します。失効した Assertion も問題なく解決されます。ペイロードに失効フラグが付いているだけです。ウィジェットが「リクエストが成功したか」しか見ていなければ、失効した資格情報も検証済みとして表示されます。これはウィジェットがないより悪い状況です。

公開の人間向けページと API エンドポイント

https://badges.ninja で発行されたすべての授与には公開 URL があり、受領者はそのまま共有できます。閲覧にサインアップは不要です。

Public credential page

受領者の多くは、このページを LinkedIn、履歴書、メール署名からリンクします。人が読むために作られており、バッジ画像、発行者、発行日が表示され、あなたの側では何もしなくても動作します。以下の API エンドポイントは同じ Assertion を構造化された JSON-LD として返します。自社の UI を作るときは、こちらを使います。

エンドポイント

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

認証ヘッダーは不要です。意図的に公開されています。Open Badges の目的は、雇用主、採用担当者、資格審査機関など誰もがアカウントなしで資格情報を検証できることだからです。返される Assertion の正確なフィールド名を含む完全な仕様は、検証ドキュメントにあります。

授与の GUID を渡します。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 に置き換えてください。クライアント側の統合はこれで全部です。CSS と格闘することになる iframe の境界はなく、検証チェックと一緒に独自の分析やトラッキングピクセルを持ち込むサードパーティのスクリプトもありません。

サーバー側:Node

バックエンドで検証したい場合もあるでしょう。たとえば、自分が管理するプロフィールページに「Verified Partner」バッジを表示する前や、オンボーディングの流れで資格情報を証明として受け付ける前です。その場合、DOM の部分を除けば fetch はまったく同じです。

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 以外のレスポンスは例外を投げずに「見つからない」として扱います。公開の検証エンドポイントにとって、壊れた award ID や推測された award ID は例外ではなく想定内の入力です。捕捉されたエラーではなく、通常の分岐として処理してください。

他人の UI を埋め込まず、結果を自分でスタイリングする

上のウィジェットは自分の DOM に対する単なる fetch 呼び出しなので、見た目はすべて自分で決められます。よくあるパターンは、レスポンスからそのまま 3 つの表示状態に対応させる方法です。

  • Verified — 緑のチェックマークと「Verified」、発行日。サイトのバッジやタグのコンポーネントに合わせてスタイリングします。
  • Revoked — 控えめな色か赤で、失効理由を隠さずに表示します。受領者にも第三者にも、黙って失敗するより正直な「理由」のほうが役立ちます。
  • Not found — 見た目は「失効」と同じ扱い(偽の成功状態は表示しない)にしつつ、文言は変えます。不正な ID と意図的な失効は、ページを読む人にとって別の状況だからです。

コンポーネントライブラリは不要です。verified / revoked / not_found の switch と 3 つの CSS クラスで足ります。

キャッシュでエンドポイントに負荷をかけない

検証エンドポイントは公開・認証不要なので、リクエストごとのクレジット費用は気にする必要がありません。ただし、クライアント側で呼び出してキャッシュしなければ、ページを読み込むたびにネットワークの往復が発生します。すべての訪問者に同じバッジを表示するページ(たとえばフッターの「Verified Partner」バッジ)では、リクエストのたびに再取得せず、短い TTL でサーバー側にキャッシュしましょう。失効は頻繁に起きるシグナルではないので、5 〜 15 分で十分です。

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;
}

訪問者ごとのウィジェット(受領者が個人サイトに自分の資格情報を埋め込む場合)では、キャッシュは使わず、ページ読み込み時に毎回呼び出してください。その規模なら追加のリクエストは無視できます。

badges.ninja が受領者にすでに提供しているもの

完全にカスタムの埋め込みを作る前に、すでに用意されているものを知っておく価値があります。すべての授与の公開ページには、受領者がそのままコピーできる共有用・埋め込み用のスニペットが用意されています。受領者側のコードは不要です。

Public share page with social buttons + embed snippets

目的が「受領者が自分のバッジが本物であると示せるようにする」ことなら、badges.ninja/awards/<guid> ページへのリンクのほうが、カスタムウィジェットを作るより簡単なことが多いです。先ほどの人間向けページであり、検証済みと失効の区別もすでに処理してくれます。この記事のカスタムウィジェットを作るのは、検証ステータスを自社プロダクトの UI の中に表示したい場合です。たとえばパートナーディレクトリ、社内の監査ダッシュボード、候補者が申告した認定資格を確認する採用ツールなど、別のサイトへのリンクでは UX として不適切なケースです。

なぜ iframe やベンダーのウィジェットではないのか

プラットフォームによっては、検証ステータスを埋め込むために <iframe src="https://vendor.example.com/verify-widget"> のようなスニペットを渡してきます。動作はしますが、実際のコストがあります。中身をサイトに合わせて再スタイリングできず(ベンダーが用意した CSS に縛られます)、Content-Security-Policy で明示的に許可が必要なクロスオリジンのドキュメントが増え、訪問者が検証ステータスに関心があるかどうかにかかわらず、ページを読み込むたびにサードパーティの iframe ホストへリクエストが飛びます。2 回の呼び出しで済む fetch ウィジェットなら、この 3 つをすべて避けられます。DOM も見た目も自分のもので、呼び出すと決めたときにだけ動作します。

検証チェーン全体をたどる

ステータスウィジェットより徹底したものを作る場合、たとえば資格情報をまとめてチェックする監査ツールやコンプライアンスレポートなら、Assertion で止めずにチェーン全体をたどってください。Assertion は badge URL(BadgeClass:名前、説明、基準)を参照し、BadgeClass はさらに issuer URL(組織プロファイル)を参照します。厳密な検証では 3 つすべてを解決し、内部で整合していることを確認します。バッジが主張どおりの発行者に属していること、Assertion が主張どおりのバッジを参照していることです。最上位の Assertion が 200 を返すだけでは足りません。単一の資格情報のステータスウィジェットにはそこまでの深さは不要ですが、パートナーや監査人に「検証は実際どこまで深く行っているのですか?」と聞かれたときのために、この構造を知っておく価値があります。

よくある間違い

  • HTTP ステータスだけを確認する。 前述のとおり、失効した Assertion も解決されます。ステータスコードだけで検証済みと判断せず、必ず assertion.revoked を読んでください。
  • 共有コンポーネントに award ID をハードコードする。 ウィジェットを多くの受領者のページで再利用するなら、award ID はスクリプトに直接書かず、パラメーター(data 属性、クエリ文字列、prop)として受け取ってください。
  • ネットワークエラーを黙って握りつぶす。 何も記録しない try/catch で、本当に存在しないバッジと同じ「見つかりません」を表示すると、後のデバッグが大変になります。少なくともログ上では、「ネットワーク呼び出しが失敗した」と「API が否定した」を区別してください。
  • クライアントサイドアプリで再レンダリングのたびに再検証する。 ウィジェットが頻繁に再レンダリングされる React や Vue のコンポーネント内にある場合は、再レンダリングのたびではなく、マウントごとに 1 回(または上のキャッシュの節で述べた TTL ごとに 1 回)だけ検証してください。

クレジットは消費しますか?無料プランでも使えますか?

いいえ、消費しません。検証エンドポイントは、発行を管理するクレジットの仕組みの外にあります。バッジが有効かどうかの確認に発行者の費用はかからず、呼び出す訪問者にアカウント、API キー、プランは一切不要です。Free、Starter、Pro のどのプランで発行されたバッジでも同じです。検証は資格情報そのものの性質であり、発行者のサブスクリプションで制限される機能ではありません。だからこそ、アクセスが予測できない公開ページ(採用ページやパートナーディレクトリなど)にも、アカウントに紐づくレート制限を気にせず安全にこのウィジェットを置けます。呼び出すのは認証付き API ルートではなく、読み取り専用の公開エンドポイントです。

1 点、明確にしておきたいことがあります。エンドポイントが返す Assertion JSON には、受領者の生のメールアドレスは含まれません。受領者のメールはバックエンドでハッシュ化して保存されており、公開の検証レスポンスから個人の連絡先がスクレイピングされないようになっています。返ってくるのは資格情報を確認するのに十分な内容(バッジ、発行者、日付、失効ステータス)であり、award ID を総当たりする誰かのためのリード獲得ツールにはなりません。

最初の検証可能な資格情報を発行しませんか?

badges.ninja で無料で始める — ビジュアルデザイナー、公開検証ページ、PDF 証明書、Open Badge v2.0 出力。クレジットカードは不要です。

Assertion のすべてのフィールドの構造を注釈付きで確認するには、JSON-LD フィールドリファレンスをご覧ください。また、検証だけでなくプログラムから資格情報を発行する場合は、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.

ブログに戻る

関連記事