Skip to content

Verification and sharing

Once an issuance completes, a signed credential (VC-JWT) is created shortly afterwards. This guide covers how to find that credential, how to verify it, and how to share it with the recipient. You never implement verification yourself: the verdict comes from Kolleges’ public verification endpoint, and the API hands you pointers to it.

Issuance and credential are two different moments

Section titled “Issuance and credential are two different moments”

When POST /v2/clubs/{clubDomain}/issuances returns 200, the issuance record exists, but the signed credential is baked in the background a moment later. Requesting the credential in between returns 409 credential-pending. Wait for the event instead of polling.

  1. Issue. results[].resourceId in the response is the issuance id.

  2. Wait for issuance.credential_issued. When that event appears in the GET /v2/clubs/{clubDomain}/events feed, the signed credential exists. subject.id is the issuance id.

  3. Read the credential. GET /v2/clubs/{clubDomain}/issuances/{issuanceId}/credential returns { id, format, jwt, payload }. jwt is the compact JWT, payload its decoded claims.

// GET /v2/clubs/acme/events
{
"data": [
{ "id": "", "type": "issuance.credential_issued", "occurredAt": "2026-09-03T06:12:41.000Z", "subject": { "type": "issuance", "id": "1450" } },
{ "id": "", "type": "issuance.issued", "occurredAt": "2026-09-03T06:12:38.000Z", "subject": { "type": "issuance", "id": "1450" } }
],
"meta": { "nextCursor": null }
}

The credential block on the issuance detail

Section titled “The credential block on the issuance detail”

credential on GET /v2/clubs/{clubDomain}/issuances/{issuanceId} is a pointer to the credential. It is null until the credential has been baked.

Field Meaning
id The credential UUID, identical to the id inside the credential
format OB3_VC_JWT
status valid, expired or revoked; revocation wins over expiry. The signature is not checked here
issuedAt When the credential was created
shareUrl The share and verification page to send to the recipient (https://{clubDomain}.kolleges.net/credentials/{id})
verifyUrl The public verification API, returning a JSON verdict without authentication
jwtUrl The public JWT endpoint, returning the signed JWT (text/plain) without authentication

List items carry achievementId and credentialId. A null credentialId means the credential has not been baked yet.

Verifiers, wallets and LMSs that hold no partner key call the Open Badges 3.0 public surface directly. The reference is at Open Badges 3.0 · Verification.

Endpoint What it returns
GET https://api.kolleges.net/open-badges/v3/achievements/{issuanceId}/verify isValid (signature verdict), status, issuerVerified (issuer trust), payload (decoded VC)
GET https://api.kolleges.net/open-badges/v3/credentials/{credentialId} The signed JWT (text/plain)
GET https://api.kolleges.net/open-badges/v3/issuers/{domain}/did.json The issuer’s DID Document (public keys)
GET https://api.kolleges.net/open-badges/v3/issuers/{domain}/status-lists/revocation The revocation status list (Bitstring Status List)

payload.vc.credentialSubject.achievement in the verify response carries the criteria narrative, the skills (tag, alignment) and the engraved description exactly as they were at issuance. That is the frozen record described in Competency data.

// GET https://api.kolleges.net/open-badges/v3/achievements/1450/verify
{
"statusCode": 200,
"isValid": true,
"status": "valid",
"issuerVerified": true,
"payload": {
"iss": "did:web:api.kolleges.net:open-badges:v3:issuers:acme",
"vc": {
"credentialSubject": {
"achievement": {
"achievementType": "CertificateOfCompletion",
"criteria": { "narrative": "1. Complete the 12-week curriculum\n2. Present the final project (https://acme.example/demo-day)" },
"tag": ["React", "TypeScript", "Teamwork"]
}
}
}
}
}

isValid is a verdict about the credential; issuerVerified is a commercial trust attestation about the issuing organisation. They are independent axes, so a signature can be valid while issuerVerified is false.

Send the recipient the shareUrl. That page opens without authentication, shows the credential’s current verdict, and is the entry point for the recipient to take the credential into a wallet or onto social media. A revoked credential shows as revoked there too.

credential { id, shareUrl, verifyUrl } | null on GET /v1/achievements/{id} is the same pointer. You can read it back immediately using issued[].id from the bulk-issue response. The signed JWT itself comes from the public credentials/{credentialId} endpoint above.