Nhúng xác minh huy hiệu vào website của bạn (không iframe, không SDK)
Thêm một đoạn JavaScript nhỏ (hoặc một lệnh fetch phía máy chủ) vào trang của bạn để xác minh bất kỳ Open Badge nào qua endpoint JSON-LD công khai. Không iframe, không trình theo dõi của bên thứ ba.
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.
Nếu bạn cấp credential, sớm muộn gì cũng có người hỏi: “Làm sao tôi biết huy hiệu này là thật?” Câu trả lời trung thực không bao giờ nên là “cứ tin vào file PNG.” Open Badge v2.0 giải quyết bằng một endpoint xác minh công khai, máy đọc được, và đặt một widget “Verified” lên website của bạn chỉ tốn khoảng mười dòng code — không iframe, không script nhúng của bên thứ ba, và không SDK.
Bài viết này chỉ chính xác cách gọi endpoint và hiển thị kết quả, bằng JavaScript thuần và ở phía máy chủ với Node và Python. Bài cũng giải thích “xác minh” thực sự kiểm tra những gì, endpoint này nằm ở đâu so với các trang dành cho người dùng mà badges.ninja đã cung cấp cho người nhận, và những lỗi khiến widget xác minh trông như đã xong nhưng thực ra chẳng chứng minh được gì.
”Verified” thực sự nghĩa là gì
Một assertion Open Badge v2.0 là tài liệu JSON-LD có cấu trúc cố định: bản thân assertion (ai nhận, khi nào), tham chiếu tới BadgeClass (huy hiệu là gì, ai định nghĩa tiêu chí), và tham chiếu tới hồ sơ tổ chức cấp (ai đứng ra bảo chứng). Khi bạn “xác minh” một huy hiệu, thực chất bạn đang kiểm tra ba việc riêng biệt:
- Assertion có tồn tại và resolve được không. ID award là thật, không phải đoán mò hay bị can thiệp.
- Nó đã bị thu hồi chưa. Tổ chức cấp có thể thu hồi huy hiệu sau đó — credential cấp nhầm, vi phạm chính sách, chứng chỉ hết hạn — và một trình xác minh đúng đắn phải hiển thị điều này, chứ không chỉ xác nhận bản ghi tồn tại.
- Chuỗi liên kết có khớp nhau không. Các tham chiếu badge và tổ chức cấp trong assertion thực sự trỏ tới những bản ghi có thật và khớp nhau, không phải ID mồ côi hay lệch nhau.
Một widget xác minh chỉ ping một URL rồi kiểm tra mã 200 sẽ bỏ lỡ hoàn toàn điểm 2. Assertion bị thu hồi vẫn resolve bình thường — chỉ là payload có kèm cờ thu hồi. Nếu widget của bạn chỉ kiểm tra “request có thành công không”, một credential đã bị thu hồi vẫn sẽ hiện là đã xác minh, còn tệ hơn là không có widget.
Trang công khai cho người dùng và endpoint API
Mọi award được cấp trên https://badges.ninja đều có một URL công khai để người nhận chia sẻ trực tiếp, người xem không cần đăng ký:

Trang đó là thứ hầu hết người nhận sẽ dẫn link từ LinkedIn, CV hoặc chữ ký email — nó dành cho con người, hiển thị hình huy hiệu, tổ chức cấp và ngày cấp, và bạn không cần làm gì để nó hoạt động. Endpoint API bên dưới trả về cùng assertion đó dưới dạng JSON-LD có cấu trúc, thứ bạn cần khi tự xây giao diện thay vì chỉ dẫn link sang trang của chúng tôi.
Endpoint
GET https://api.badges.ninja/certify-badge/award/{guid}
Không cần header xác thực — endpoint này cố ý để công khai, vì toàn bộ mục đích của Open Badges là bất kỳ ai (nhà tuyển dụng, nhà tuyển dụng nhân sự, hội đồng cấp phép) cũng có thể xác minh credential mà không cần tài khoản. Chi tiết đầy đủ về hợp đồng API, gồm tên chính xác các trường trong assertion trả về, nằm ở tài liệu xác minh.
Hãy truyền GUID của award, bạn có thể lấy từ hồ sơ cấp phát của mình hoặc tách ra từ liên kết badges.ninja/awards/<guid> mà người nhận chia sẻ.
JS thuần: fetch và hiển thị
Đây là toàn bộ widget. Không cần bước build, không gói npm, dán thẳng vào bất kỳ trang nào:
<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>
Hãy thay REPLACE_WITH_AWARD_ID bằng GUID award thật. Đó là toàn bộ phần tích hợp phía client: không có ranh giới iframe để vật lộn với CSS, và không có script bên thứ ba kéo theo analytics hay pixel theo dõi riêng bên cạnh bước kiểm tra xác minh.
Phía máy chủ: Node
Nếu bạn muốn xác minh ở backend — chẳng hạn trước khi hiển thị huy hiệu “Verified Partner” trên trang hồ sơ do bạn quản lý, hoặc trước khi chấp nhận một credential làm bằng chứng trong luồng onboarding — thì lệnh fetch giống hệt, chỉ bỏ phần 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 };
}
Phía máy chủ: 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")}
Cả hai ví dụ đều coi phản hồi khác 200 là “not found” thay vì ném lỗi. Một ID award hỏng hoặc đoán bừa là đầu vào hoàn toàn có thể xảy ra với endpoint xác minh công khai, không phải tình huống bất thường, nên hãy xử lý nó như một nhánh bình thường thay vì một lỗi bị bắt.
Tạo kiểu cho kết quả thay vì nhúng giao diện của người khác
Vì widget trên chỉ là một lời gọi fetch vào DOM của chính bạn, mọi pixel đều do bạn quyết định. Một mẫu phổ biến là ba trạng thái hiển thị ánh xạ thẳng từ phản hồi:
- Verified — dấu tích xanh cùng chữ “Verified” và ngày cấp, tạo kiểu khớp với các thành phần badge/tag của trang bạn.
- Revoked — trạng thái mờ hoặc đỏ hiển thị lý do thu hồi thay vì giấu đi. Cả người nhận lẫn bên thứ ba đều được lợi từ một lời giải thích “vì sao” trung thực, thay vì một thất bại âm thầm.
- Not found — về mặt hiển thị hãy xử lý giống “revoked” (đừng hiện trạng thái thành công giả), nhưng dùng nội dung khác, vì ID sai và việc cố ý thu hồi là hai tình huống khác nhau với người đọc trang.
Không cái nào cần thư viện component. Một switch trên verified / revoked / not_found và ba class CSS là đủ.
Caching, để không dồn dập gọi endpoint
Endpoint xác minh là công khai và không cần xác thực, nghĩa là không phải lo chi phí credit cho mỗi request — nhưng nếu gọi từ phía client mà không cache thì vẫn là một vòng gọi mạng ở mỗi lần tải trang. Với trang hiển thị cùng một huy hiệu cho mọi khách (ví dụ huy hiệu “Verified Partner” ở chân trang), hãy cache kết quả phía máy chủ với TTL ngắn — năm đến mười lăm phút là thừa, vì việc thu hồi không phải tín hiệu thay đổi nhanh — thay vì fetch lại ở mỗi request:
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;
}
Với widget theo từng khách (người nhận nhúng credential của chính họ lên trang cá nhân), hãy bỏ qua cache và gọi mới khi tải trang — một request thêm là không đáng kể ở quy mô đó.
Những gì badges.ninja đã cung cấp sẵn cho người nhận
Trước khi tự xây một bản nhúng hoàn toàn tùy biến, bạn nên biết những gì đã có sẵn. Trang công khai của mỗi award đã kèm các đoạn chia sẻ và nhúng làm sẵn để người nhận sao chép trực tiếp, không cần viết code:

Nếu nhu cầu của bạn là “để người nhận chứng minh huy hiệu của họ là thật”, thì dẫn link tới trang badges.ninja/awards/<guid> của họ thường đơn giản hơn nhiều so với tự xây widget — đó chính là trang dành cho người dùng đã nêu ở trên, và nó đã xử lý sẵn sự khác biệt giữa verified/revoked. Hãy xây widget tùy biến trong bài này khi bạn cần hiển thị trạng thái xác minh ngay trong giao diện sản phẩm của chính bạn — một danh bạ đối tác, bảng điều khiển kiểm toán nội bộ, công cụ tuyển dụng kiểm tra chứng chỉ ứng viên khai báo — nơi việc đưa người dùng sang một trang khác không phải là trải nghiệm phù hợp.
Vì sao không dùng iframe hay widget của nhà cung cấp?
Một số nền tảng đưa bạn đoạn mã như <iframe src="https://vendor.example.com/verify-widget"> để nhúng trạng thái xác minh. Nó chạy được, nhưng đi kèm những cái giá thực sự: bạn không thể tạo kiểu lại nội dung cho khớp với trang của mình (kẹt với CSS mà nhà cung cấp gửi kèm), nó thêm một tài liệu cross-origin mà Content-Security-Policy của bạn phải cho phép tường minh, và mỗi lần tải trang đều gửi request tới host iframe của bên thứ ba dù khách có quan tâm đến trạng thái xác minh hay không. Một widget fetch hai lệnh gọi tránh được cả ba — là DOM của bạn, kiểu dáng của bạn, và chỉ chạy khi bạn quyết định gọi.
Đi hết chuỗi xác minh
Nếu bạn xây thứ gì kỹ lưỡng hơn một widget trạng thái — chẳng hạn công cụ kiểm toán kiểm tra hàng loạt credential, hay báo cáo tuân thủ — hãy đi hết cả chuỗi thay vì dừng ở assertion. Assertion tham chiếu một URL badge (BadgeClass: tên, mô tả, tiêu chí), và BadgeClass lại tham chiếu một URL issuer (hồ sơ tổ chức). Một trình xác minh nghiêm ngặt resolve cả ba và xác nhận chúng nhất quán với nhau — huy hiệu thực sự thuộc về tổ chức cấp mà nó nhận, assertion thực sự tham chiếu đúng huy hiệu mà nó nhận — chứ không chỉ assertion cấp cao nhất trả về 200. Mức sâu đó vượt quá nhu cầu của một widget trạng thái cho một credential, nhưng nên biết là có, phòng khi đối tác hoặc kiểm toán viên hỏi: “việc xác minh của bạn thực sự đi sâu đến đâu?”
Những lỗi thường gặp cần tránh
- Chỉ kiểm tra trạng thái HTTP. Như đã nói ở trên, assertion bị thu hồi vẫn resolve. Luôn đọc
assertion.revoked, đừng suy ra việc xác minh chỉ từ mã trạng thái. - Hardcode ID award trong component dùng chung. Nếu widget định dùng lại trên trang của nhiều người nhận, hãy nhận ID award làm tham số (data attribute, query string, prop), không phải một giá trị cố định gắn cứng trong script.
- Nuốt lỗi mạng trong im lặng. Một
try/catchkhông ghi log gì và hiện cùng dòng “not found” như huy hiệu thực sự không tồn tại sẽ khiến việc gỡ lỗi sau này rất khổ. Hãy phân biệt “lời gọi mạng thất bại” với “API trả lời là không” trong các nhánh lỗi, ít nhất là trong log. - Xác minh lại ở mỗi lần render trong ứng dụng phía client. Nếu widget nằm trong component React hoặc Vue render lại thường xuyên, hãy xác minh một lần mỗi lần mount (hoặc một lần mỗi cửa sổ TTL cache, theo phần caching ở trên), không phải mỗi lần render lại.
Việc này có tốn credit không, và có chạy trên gói miễn phí không?
Không. Endpoint xác minh cố ý nằm ngoài hệ thống credit chi phối việc cấp huy hiệu — kiểm tra một huy hiệu có hợp lệ hay không không tốn gì của bên cấp, và người gọi không cần tài khoản, API key hay bất kỳ gói nào. Điều này đúng dù huy hiệu được cấp trên gói Free, Starter hay Pro: xác minh là thuộc tính của chính credential, không phải tính năng bị khóa theo gói đăng ký của bên cấp. Đó cũng là lý do bạn có thể yên tâm đặt widget này trên trang công khai có lưu lượng khó đoán (trang tuyển dụng, danh bạ đối tác) mà không lo giới hạn tốc độ gắn với tài khoản của mình — bạn đang gọi một endpoint công khai chỉ đọc, không phải một route API cần xác thực.
Có một điều cần nói rõ: JSON assertion do endpoint trả về không chứa địa chỉ email gốc của người nhận. Email người nhận được lưu dưới dạng băm ở backend chính là để phản hồi xác minh công khai không thể bị cào lấy thông tin liên hệ cá nhân — những gì bạn nhận được đủ để xác nhận credential (huy hiệu, tổ chức cấp, ngày tháng, trạng thái thu hồi) mà không biến endpoint thành công cụ tìm khách hàng cho bất kỳ ai lặp qua các ID award.
Sẵn sàng cấp credential có thể xác minh đầu tiên của bạn?
Bắt đầu miễn phí tại badges.ninja — trình thiết kế trực quan, trang xác minh công khai, chứng chỉ PDF, đầu ra Open Badge v2.0. Không cần thẻ tín dụng.
Để xem cấu trúc có chú thích đầy đủ của từng trường trong assertion, hãy đọc tài liệu tham chiếu trường JSON-LD. Và nếu bạn cấp credential bằng lập trình thay vì chỉ xác minh, hướng dẫn nhanh API trình bày phần xác thực và lời gọi tạo award tạo ra output của endpoint này.
Bài viết này được thực hiện như thế nào
Một số bài viết trên blog này được soạn với sự hỗ trợ của trợ lý AI, sau đó được đội ngũ Badges Ninja xem xét, kiểm chứng và biên tập trước khi xuất bản. Mọi đoạn code và mức giá đều được đối chiếu với sản phẩm thực tế. Tìm hiểu thêm về quy trình biên tập và AI của chúng tôi tại trang quy trình biên tập .

Về tác giả
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.

