Badgeverificatie op je website insluiten (zonder iframe, zonder SDK)

Voeg een klein JavaScript-fragment (of een server-side fetch) toe aan je site dat elke Open Badge verifieert via een openbaar JSON-LD-endpoint. Geen iframe, geen tracker van derden.

Nacho Coll Door Bijgewerkt 10 min leestijd
Voeg een klein JavaScript-fragment (of een server-side fetch) toe aan je site dat elke Open Badge verifieert via een openbaar JSON-LD-endpoint. Geen iframe, geen tracker van derden.

Als je credentials uitgeeft, vraagt vroeg of laat iemand: „Hoe weet ik dat deze badge echt is?” Het eerlijke antwoord mag nooit zijn: „Vertrouw op de PNG.” Open Badge v2.0 lost dat op met een openbaar, machineleesbaar verificatie-endpoint, en een „Geverifieerd”-widget op je eigen site kost ongeveer tien regels code — geen iframe, geen embed-script van een derde partij en geen SDK.

Dit artikel laat precies zien hoe je het endpoint aanroept en het resultaat toont, in plain JavaScript en server-side in Node en Python. Het behandelt ook wat „verificatie” daadwerkelijk controleert, hoe het endpoint zich verhoudt tot de pagina’s voor mensen die badges.ninja ontvangers al biedt, en de fouten waardoor een verificatiewidget af lijkt terwijl hij stiekem niets bewijst.

Wat „geverifieerd” echt betekent

Een Open Badge v2.0-assertion is een JSON-LD-document met een vaste vorm: de assertion zelf (wie hem ontving, wanneer), een verwijzing naar de BadgeClass (wat het is, wie de criteria bepaalt) en een verwijzing naar het profiel van de uitgever (wie ervoor instaat). Als je een badge „verifieert”, controleer je eigenlijk drie aparte dingen:

  1. Bestaat de assertion en wordt hij opgelost. De award-ID is echt, niet verzonnen of aangepast.
  2. Is hij ingetrokken. Een uitgever kan een badge achteraf intrekken — een credential die per ongeluk is uitgegeven, een overtreding van het beleid, een certificering die is verlopen — en een correcte verificateur moet dat tonen, niet alleen bevestigen dat het record bestaat.
  3. Klopt de keten. De verwijzingen van de assertion naar badge en uitgever leiden naar echte, bij elkaar passende records, niet naar verweesde of niet-overeenkomende ID’s.

Een verificatiewidget die alleen een URL aanroept en op een 200-status controleert, mist punt 2 volledig. Een ingetrokken assertion wordt gewoon opgelost — hij draagt alleen een intrekkingsvlag in de payload. Als je widget alleen checkt „is het verzoek gelukt”, wordt een ingetrokken credential als geverifieerd getoond, en dat is erger dan helemaal geen widget.

De openbare pagina voor mensen vs. het API-endpoint

Elke award die op https://badges.ninja wordt uitgegeven, krijgt een openbare URL die ontvangers direct kunnen delen, zonder dat je je hoeft aan te melden om hem te bekijken:

Openbare credentialpagina

Die pagina is waar de meeste ontvangers naar linken vanaf LinkedIn, een cv of een e-mailhandtekening — hij is gemaakt voor mensen, toont de badgeafbeelding, de uitgever en de uitgiftedatum, en heeft niets van jou nodig om te werken. Het API-endpoint hieronder geeft dezelfde onderliggende assertion terug als gestructureerde JSON-LD, en dat wil je als je je eigen UI bouwt in plaats van alleen naar de onze te linken.

Het endpoint

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

Geen auth-header nodig — dit is bewust openbaar, want het hele punt van Open Badges is dat iedereen (een werkgever, een recruiter, een beroepsorganisatie) een credential kan verifiëren zonder account. De volledige details van het contract, inclusief de exacte veldnamen in de teruggegeven assertion, staan in de verificatiedocumentatie.

Geef de GUID van de award mee, die je uit je eigen uitgiftegegevens haalt of uit een badges.ninja/awards/<guid>-link die een ontvanger met je heeft gedeeld.

Vanilla JS: ophalen en weergeven

Dit is de hele widget. Geen buildstap, geen npm-pakket, je zet hem zo op elke pagina:

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

Vervang REPLACE_WITH_AWARD_ID door een echte award-GUID. Dat is de volledige client-side integratie: geen iframe-grens waar je met CSS tegenop moet boksen, en geen script van een derde partij dat zijn eigen analytics of trackingpixels naast de verificatiecheck meebrengt.

Server-side: Node

Als je liever op je backend verifieert — bijvoorbeeld voordat je een „Geverifieerde partner”-badge toont op een profielpagina die je zelf beheert, of voordat je een credential als bewijs accepteert in een onboardingflow — is de fetch identiek, minus het DOM-deel:

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

Server-side: 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")}

Beide voorbeelden behandelen een respons die niet 200 is als „niet gevonden” in plaats van een fout te gooien. Een kapotte of geraden award-ID is voor een openbaar verificatie-endpoint een verwachte invoer, geen uitzondering, dus behandel dat als een gewone tak in je code en niet als een afgevangen fout.

Het resultaat stylen in plaats van andermans UI insluiten

Omdat de widget hierboven gewoon een fetch-aanroep is die in je eigen DOM schrijft, ben jij de baas over elke pixel. Een gangbaar patroon is drie visuele toestanden, rechtstreeks afgeleid van de respons:

  • Geverifieerd — een groen vinkje met „Geverifieerd” en de uitgiftedatum, opgemaakt zoals de badge-/tagcomponenten van je site.
  • Ingetrokken — een gedempte of rode toestand die de reden van intrekking toont in plaats van die te verbergen. Zowel ontvangers als derden hebben baat bij een eerlijk „waarom”, niet bij een stille fout.
  • Niet gevonden — behandel dit visueel net als „ingetrokken” (toon geen nep-successtatus), maar met andere tekst, want een foute ID en een bewuste intrekking zijn verschillende situaties voor wie de pagina leest.

Hier is geen componentenbibliotheek voor nodig. Een switch op verified / revoked / not_found en drie CSS-klassen volstaan.

Caching, zodat je het endpoint niet bestookt

Het verificatie-endpoint is openbaar en niet geauthenticeerd, wat ook betekent dat je je geen zorgen hoeft te maken over kosten per verzoek in credits — maar het blijft een netwerkaanroep bij elke paginalading als je het client-side zonder caching aanroept. Voor een pagina die dezelfde badge aan elke bezoeker toont (een „Geverifieerde partner”-badge in de footer, bijvoorbeeld), cache je het resultaat server-side met een korte TTL — vijf tot vijftien minuten is ruim genoeg, want intrekkingen zijn geen snel veranderend signaal — in plaats van bij elk verzoek opnieuw op te halen:

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

Voor een widget per bezoeker (een ontvanger die zijn eigen credential op een persoonlijke site insluit) sla je de cache over en roep je het gewoon vers aan bij het laden van de pagina — het extra verzoek is op die schaal verwaarloosbaar.

Wat badges.ninja ontvangers al biedt

Voordat je een volledig eigen embed bouwt, is het goed om te weten wat er al kant-en-klaar is. De openbare pagina van elke award bevat kant-en-klare deel- en embedfragmenten die ontvangers direct kunnen kopiëren, zonder dat ze zelf code hoeven te schrijven:

Openbare deelpagina met socialemediaknoppen en embedfragmenten

Als jouw use case „een ontvanger laten tonen dat zijn badge echt is” is, is linken naar zijn badges.ninja/awards/<guid>-pagina vaak eenvoudiger dan een eigen widget bouwen — het is de eerder getoonde pagina voor mensen en die regelt het onderscheid tussen geverifieerd en ingetrokken al voor je. Bouw de eigen widget uit dit artikel als je de verificatiestatus binnen de UI van je eigen product wilt tonen — een partneroverzicht, een intern auditdashboard, een wervingstool die de opgegeven certificeringen van een kandidaat controleert — waar linken naar een andere site niet de juiste UX is.

Waarom geen iframe of widget van een leverancier?

Sommige platforms geven je een fragment zoals <iframe src="https://vendor.example.com/verify-widget"> om de verificatiestatus in te sluiten. Het werkt, maar het heeft echte nadelen: je kunt de inhoud niet aan je site aanpassen (je zit vast aan de CSS die de leverancier heeft meegeleverd), het introduceert een cross-origin-document dat je Content-Security-Policy expliciet moet toestaan, en bij elke paginalading gaat er een verzoek naar de iframehost van een derde, of de bezoeker nu om de verificatiestatus geeft of niet. Een fetch-widget met twee aanroepen vermijdt alle drie: het is jouw DOM, jouw styling, en het draait alleen als jij besluit het aan te roepen.

De hele verificatieketen doorlopen

Als je iets grondigers bouwt dan een statuswidget — een auditprogramma dat een reeks credentials controleert, of een compliancerapport — loop dan de hele keten door in plaats van bij de assertion te stoppen. De assertion verwijst naar een badge-URL (de BadgeClass: naam, beschrijving, criteria) en de BadgeClass verwijst op zijn beurt naar een issuer-URL (het organisatieprofiel). Een strenge verificateur lost alle drie op en bevestigt dat ze onderling consistent zijn — de badge hoort echt bij de uitgever die hij opgeeft, de assertion verwijst echt naar de badge die hij opgeeft — en niet alleen dat de assertion op het hoogste niveau een 200 teruggeeft. Dat is meer diepgang dan een statuswidget voor één credential nodig heeft, maar het is goed om te weten dat deze structuur bestaat als een partner of auditor je ooit vraagt: „hoe diep gaat jullie verificatie eigenlijk?”

Veelgemaakte fouten

  • Alleen de HTTP-status controleren. Zoals hierboven beschreven wordt een ingetrokken assertion gewoon opgelost. Lees altijd assertion.revoked en leid verificatie niet alleen af uit de statuscode.
  • De award-ID hardcoden in een gedeelde component. Als de widget herbruikbaar moet zijn op de pagina’s van veel ontvangers, neem de award-ID dan als parameter (een data-attribuut, een querystring, een prop) en niet als letterlijke waarde in het script.
  • Netwerkfouten stilzwijgend negeren. Een try/catch die niets logt en dezelfde „niet gevonden”-tekst toont als bij een echt ontbrekende badge, maakt debuggen later een crime. Maak in je foutafhandeling onderscheid tussen „de netwerkaanroep is mislukt” en „de API zei nee”, al is het maar in de logs.
  • Bij elke render opnieuw verifiëren in een client-side app. Als de widget in een React- of Vue-component zit die vaak opnieuw rendert, verifieer dan één keer per mount (of één keer per gecachet TTL-venster, zie het cachingdeel), niet bij elke re-render.

Kost dit credits, en werkt het met het gratis plan?

Nee. Het verificatie-endpoint staat bewust buiten het creditsysteem dat het uitgeven regelt — controleren of een badge geldig is kost de uitgever niets, en de bezoeker die het aanroept hoeft geen account, API-sleutel of plan te hebben. Dat geldt ongeacht of de badge is uitgegeven onder een Free-, Starter- of Pro-plan: verificatie is een eigenschap van de credential zelf, geen functie die achter het abonnement van de uitgever zit. Het is ook waarom je deze widget veilig op een openbare pagina met onvoorspelbaar verkeer kunt zetten (een vacaturepagina, een partneroverzicht) zonder je zorgen te maken over rate limits die aan je account zijn gekoppeld — je roept een openbaar, alleen-lezen endpoint aan, geen geauthenticeerde API-route.

Eén ding is het waard om expliciet te zeggen: de assertion-JSON die het endpoint teruggeeft, bevat niet het onversleutelde e-mailadres van de ontvanger. Het e-mailadres van de ontvanger wordt op de backend gehasht opgeslagen, juist zodat een openbare verificatierespons niet kan worden afgeschraapt voor persoonlijke contactgegevens — wat je terugkrijgt is genoeg om de credential te bevestigen (badge, uitgever, datums, intrekkingsstatus) zonder het endpoint te veranderen in een leadgeneratietool voor iedereen die award-ID’s afloopt.

Klaar om je eerste verifieerbare credential uit te geven?

Begin gratis op badges.ninja — visuele designer, openbare verificatiepagina, pdf-certificaat, Open Badge v2.0-output. Geen creditcard nodig.

Voor de volledige geannoteerde structuur van elk veld in een assertion, zie de JSON-LD-veldreferentie. En als je credentials programmatisch uitgeeft in plaats van ze alleen te verifiëren, behandelt de API-quickstart de authenticatie en de aanroep om awards aan te maken waar de output van dit endpoint vandaan komt.

Nacho Coll

Over de auteur

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.

Terug naar blog

Gerelateerde artikelen