Revocation and status lists
Revoking a credential you have already handed out is the problem credentials have that tokens do not: the holder keeps their copy, and it stays cryptographically valid. The answer is a W3C Bitstring Status List — one signed, published bitstring per organization, with a bit reserved for each credential.
Revoke
Section titled “Revoke”Requires credentials:revoke.
POST /v1/credentials/vc_…/revokeAuthorization: Bearer sk_live_…{ "ok": true }Revocation flips that credential’s bit. The next verifier to check the list sees
notRevoked: false, and valid becomes false.
Writes a vc_revoked audit event and fires a credential.revoked webhook. An
org-scoped key can only revoke its own org’s credentials.
How it works
Section titled “How it works”Each issued credential is allocated an index in the org’s status list at issuance — atomically, so concurrent issuance never hands out the same index twice. The credential carries a pointer to its own bit:
{ "credentialStatus": { "id": "https://acme.oauth.work/status/sl_…#4217", "type": "BitstringStatusListEntry", "statusPurpose": "revocation", "statusListIndex": "4217", "statusListCredential": "https://acme.oauth.work/status/sl_…" }}The list itself is published as an org-signed credential:
GET https://acme.oauth.work/status/sl_…Content-Type: application/vc+jwtIt is a gzipped bitstring — 131,072 bits, about 16 KB uncompressed, and far smaller on the wire because a list with a handful of revocations compresses to almost nothing.
This shape is what makes revocation checkable without leaking anything. A verifier fetches the whole list and inspects one bit, so the issuer never learns which credential was being checked — quite unlike asking “is credential 4217 still valid?”
Checking it
Section titled “Checking it”The verify endpoint does it for you and reports the result in
checks.notRevoked. To check yourself:
- Read
credentialStatus.statusListCredentialfrom the credential. - Fetch it and verify its signature against the issuer’s DID document — the list is itself a signed credential, so a tampered list is detectable.
- Base64url-decode and gunzip
encodedList. - Read the bit at
statusListIndex. Set means revoked.
const list = await fetch(statusListCredential).then((r) => r.text())// verify the JWT, then:const bytes = gunzip(base64urlDecode(encodedList))const revoked = (bytes[index >> 3] & (0x80 >> (index & 7))) !== 0Foreign status lists
Section titled “Foreign status lists”If a credential points at a status list this platform does not host, verification returns not valid, with:
{ "error": "revocation status could not be verified (foreign status list)" }Failing closed here is deliberate. Treating an unverifiable status as “probably fine” would let anyone defeat revocation by pointing their credential’s status entry at a host that never answers.
What is not revocable
Section titled “What is not revocable”A credential issued without an organization gets no status list entry, and therefore cannot be
revoked. Its verification result simply has no notRevoked check — absence means “not revocable”,
not “not revoked”.
Tenant-issued credentials always get an entry.
Revocation versus expiry versus key rotation
Section titled “Revocation versus expiry versus key rotation”Three different tools, often confused:
| Effect | Timing | |
|---|---|---|
| Revocation | One credential stops verifying | Immediate, at the verifier’s next check |
| Expiry | The credential ages out | At its 90-day exp |
| Key rotation | Nothing stops verifying | Retired keys stay published on purpose |
Rotating a key does not invalidate credentials — that is the point of keeping retired keys published. To pull a credential, revoke it.
Listing what you issued
Section titled “Listing what you issued”GET /v1/organizations/org_01J…/credentials?limit=50&after=…Authorization: Bearer sk_live_…Cursor-paginated, showing type, format, holder, expiry, and status — which is where to look when you need to find the credential to revoke.