在你的网站上嵌入徽章验证(无需 iframe,无需 SDK)
在你的网站中加入一小段 JavaScript 代码(或一次服务端 fetch),即可通过公开的 JSON-LD 端点验证任何 Open Badge。无 iframe,无第三方追踪器。
如果你颁发凭证,迟早会有人问:“我怎么知道这枚徽章是真的?”诚实的回答绝不该是“相信这张 PNG 就行”。Open Badge v2.0 通过一个公开的、机器可读的验证端点解决了这个问题,而在你自己的网站上放一个“已验证”小组件,大约只需十行代码——不用 iframe,不用第三方嵌入脚本,也不用 SDK。
本文会准确展示如何调用该端点并渲染结果,包括原生 JavaScript,以及 Node 和 Python 的服务端写法。我们还会讲清楚“验证”到底检查了什么,这个端点与 badges.ninja 已经为获得者提供的面向用户的页面之间是什么关系,以及那些让验证小组件看起来大功告成、实际却什么也没证明的常见错误。
“已验证”到底意味着什么
Open Badge v2.0 的断言(assertion)是一份结构固定的 JSON-LD 文档:断言本身(谁获得了它、何时获得)、对 BadgeClass 的引用(它是什么、由谁定义标准),以及对颁发者资料的引用(谁为它背书)。当你“验证”一枚徽章时,实际上是在检查三件不同的事:
- 断言是否存在且可解析。 该颁发记录的 ID 是真实的,而不是瞎猜或被篡改的。
- 是否已被撤销。 颁发者可以在事后撤销徽章——比如误发的凭证、违反政策、认证已过期——正确的验证方必须把这一点呈现出来,而不能只确认记录存在。
- 整条链是否一致。 断言中对徽章和颁发者的引用,确实指向真实且相互匹配的记录,而不是孤立或不匹配的 ID。
只请求某个 URL 并检查是否返回 200 状态的验证小组件,完全漏掉了第 2 点。已撤销的断言依然可以正常解析——只是在载荷中带有撤销标记。如果你的小组件只检查“请求是否成功”,已撤销的凭证就会显示为已验证,这比没有小组件还糟糕。
面向用户的公开页面 vs. API 端点
在 https://badges.ninja 上颁发的每一份奖项都有一个公开 URL,获得者可以直接分享,查看时无需注册:

大多数获得者会把这个页面链接到 LinkedIn、简历或邮件签名里——它是为人设计的,展示徽章图片、颁发者和颁发日期,你无需做任何事它就能工作。下面的 API 端点则以结构化 JSON-LD 的形式返回同一份断言,适合你在构建自己的界面、而不只是链接到我们页面的时候使用。
端点
GET https://api.badges.ninja/certify-badge/award/{guid}
无需认证头——这是有意设计成公开的,因为 Open Badges 的全部意义就在于任何人(雇主、招聘人员、许可委员会)都能在没有账号的情况下验证凭证。完整的接口约定,包括返回断言中的确切字段名,请见验证文档。
传入奖项的 GUID 即可,你可以从自己的颁发记录中获得,也可以从获得者分享给你的 badges.ninja/awards/<guid> 链接中解析出来。
原生 JS:fetch 并渲染
这就是完整的小组件。无需构建步骤,无需 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
如果你更想在后端验证——比如在你控制的个人资料页上展示“认证合作伙伴”徽章之前,或者在入职流程中把某份凭证当作证明接受之前——fetch 的写法完全相同,只是少了 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 是预料之中的输入,而不是异常情况,所以应把它当作正常分支处理,而不是捕获错误。
自己设计结果样式,而不是嵌入别人的界面
由于上面的小组件只是向你自己的 DOM 发起一次 fetch 调用,每一个像素都由你掌控。一种常见做法是根据响应直接映射出三种视觉状态:
- 已验证——绿色对勾加上“已验证”和颁发日期,样式与你网站的徽章/标签组件保持一致。
- 已撤销——使用柔和或红色的状态,并显示撤销原因而不是隐藏它。获得者和第三方都能从诚实的“为什么”中受益,而不是看到一次无声的失败。
- 未找到——在视觉上与“已撤销”同等对待(不要渲染虚假的成功状态),但文案不同,因为对阅读页面的人来说,无效 ID 和有意撤销是两种不同的情况。
这一切都不需要组件库。对 verified / revoked / not_found 做一个 switch,再配上三个 CSS 类就够了。
缓存,避免频繁请求端点
验证端点是公开且无需认证的,这也意味着不用担心每次请求的积分成本——但如果在客户端调用且不做缓存,每次页面加载仍然是一次网络往返。对于向每位访客展示同一枚徽章的页面(例如页脚的“认证合作伙伴”徽章),请在服务端用较短的 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;
}
对于按访客区分的小组件(获得者在个人网站上嵌入自己的凭证),可以跳过缓存,每次页面加载时直接重新调用——在这种规模下,多一次请求可以忽略不计。
badges.ninja 已经为获得者提供了什么
在构建完全自定义的嵌入之前,值得先了解一下现成可用的功能。每份奖项的公开页面都包含现成的分享和嵌入代码片段,获得者可以直接复制,无需自己写任何代码:

如果你的使用场景是“让获得者展示自己的徽章是真的”,那么链接到他们的 badges.ninja/awards/<guid> 页面往往比构建自定义小组件更简单——它就是前面展示的面向用户的页面,并且已经替你处理好了“已验证”与“已撤销”的区分。当你需要把验证状态渲染在你自己产品的界面内部时——比如合作伙伴目录、内部审计仪表盘、核对候选人所声称认证的招聘工具——也就是跳转到另一个网站并不是合适体验的场景,再构建本文介绍的自定义小组件。
为什么不用 iframe 或供应商的小组件?
一些平台会给你一段类似 <iframe src="https://vendor.example.com/verify-widget"> 的代码来嵌入验证状态。它能用,但代价是实实在在的:你无法重新设计内容样式来匹配你的网站(只能接受供应商自带的 CSS),它会引入一个你的 Content-Security-Policy 必须明确允许的跨源文档,而且无论访客是否关心验证状态,每次页面加载都会向第三方的 iframe 主机发起请求。两次调用的 fetch 小组件则避开了这三点——它是你的 DOM、你的样式,而且只在你决定调用时才运行。
走完完整的验证链
如果你要构建比状态小组件更严谨的东西——比如批量检查凭证的审计工具,或合规报告——就要走完整条链,而不是停在断言这一步。断言引用一个 badge URL(即 BadgeClass:名称、描述、标准),而 BadgeClass 又引用一个 issuer URL(组织资料)。严谨的验证方会把三者全部解析出来,并确认它们内部一致——徽章确实属于它声称的颁发者,断言确实引用了它声称的徽章——而不只是顶层断言返回 200。对于单个凭证的状态小组件来说,这有些过深,但了解这种结构是值得的,万一合作伙伴或审计方问起“你们的验证到底有多深?”
需要避免的常见错误
- 只检查 HTTP 状态。 如上所述,已撤销的断言依然可以解析。务必读取
assertion.revoked,不要仅凭状态码就推断验证通过。 - 在共享组件中硬编码奖项 ID。 如果小组件要在许多获得者的页面上复用,就应把奖项 ID 作为参数(data 属性、查询字符串、prop)传入,而不是写死在脚本里。
- 悄悄吞掉网络错误。 如果
try/catch什么都不记录,并且和真正不存在的徽章显示同样的“未找到”文字,之后调试会非常痛苦。请在错误分支中区分“网络调用失败”和“API 说不行”,至少在日志里区分。 - 在客户端应用中每次渲染都重新验证。 如果小组件位于频繁重新渲染的 React 或 Vue 组件内,应每次挂载验证一次(或按上文缓存部分所述,每个缓存 TTL 窗口验证一次),而不是每次重新渲染都验证。
这会消耗积分吗?免费方案能用吗?
不会。验证端点有意放在管理颁发的积分体系之外——检查徽章是否有效不会给颁发者带来任何花费,也不要求调用方拥有账号、API 密钥或任何方案等级。无论徽章是在 Free、Starter 还是 Pro 方案下颁发的,都是如此:验证是凭证本身的属性,而不是由颁发者订阅所限制的功能。这也是你可以放心地把这个小组件放在流量不可预测的公开页面(招聘页面、合作伙伴目录)上,而不必担心与你账号挂钩的速率限制的原因——你调用的是只读的公开端点,而不是需要认证的 API 路由。
有一点值得明确说明:该端点返回的断言 JSON 不包含获得者的原始电子邮件地址。获得者的邮箱在后端是以哈希形式存储的,正是为了让公开的验证响应无法被抓取来获取个人联系方式——你得到的信息足以确认凭证(徽章、颁发者、日期、撤销状态),却不会让这个端点变成任何人遍历奖项 ID 就能用来获取销售线索的工具。
准备好颁发你的第一份可验证凭证了吗?
在 badges.ninja 免费开始——可视化设计器、公开验证页面、PDF 证书、Open Badge v2.0 输出。无需信用卡。
如需了解断言中每个字段的完整注解结构,请参阅 JSON-LD 字段参考。如果你是通过编程方式颁发凭证,而不仅仅是验证,API 快速入门涵盖了认证方式,以及生成本端点输出内容的奖项创建调用。
本文是如何撰写的
本博客的部分文章由 AI 助手协助起草,随后由 Badges Ninja 团队审核、核实并编辑后发布。所有代码示例和价格均已对照产品实际情况核实。欲了解更多关于我们编辑与 AI 流程的信息,请查看我们的 编辑流程页面 .

关于作者
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.

