Skip to content

Verifiable Credentials

A verifiable credential is a signed, portable claim about a person or an agent that a third party can check without calling you. That is the difference from a token: a token is proof to a party that trusts your authorization server; a credential is proof to anyone who can resolve your DID document.

Credentials here are JWT-secured — VC-JWT and SD-JWT VC — with no JSON-LD canonicalization anywhere. Signing is EdDSA with the tenant’s own key, and the issuer is the tenant’s did:web:<slug>.oauth.work.

FormatValueUse it when
SD-JWT VCsd-jwt-vc (default)The holder should disclose some claims and withhold others
VC-JWTvc+jwtYou want a plain W3C VCDM 2.0 credential with no selective disclosure

Selective disclosure is why sd-jwt-vc is the default. A credential asserting employment, start date, and department lets the holder present only the part a given verifier needs.

The platform issues three, chosen from the token presented at issuance rather than from a parameter:

TypeIssued toContains
WorkCredentialA user token with work_credential scopeEmployment claims about the subject
AgentIdentityCredentialAn agent or service tokenAgent id, actor type, org, scope
DelegationCredentialA delegated token (one carrying act)The subject, the actor chain, org, scope

See issuance and wallets for how to request each.

Anyone can verify, with no credential of their own:

POST /credentials/verify
Content-Type: application/json
{ "credential": "eyJ…~…", "format": "sd-jwt-vc" }
{
"valid": true,
"format": "sd-jwt-vc",
"issuer": "did:web:acme.oauth.work",
"subject": "did:key:z6Mk…",
"claims": { "employer": "Acme Corp", "role": "Engineer" },
"checks": {
"signature": true,
"issuerDid": true,
"notExpired": true,
"notRevoked": true
}
}

Each entry is a separate assertion, and valid is not simply their conjunction — read both.

CheckMeans
signatureThe signature verifies against the issuer’s published key.
issuerDidThe kid resolved to a key in the issuer’s DID document.
notExpiredWithin its validity window.
notRevokedThe status list bit is clear.

notRevoked appears only when the credential carries a status entry. A credential issued without an organization has no status list and therefore no revocation check — its absence means “not revocable”, not “not revoked”.

A failure reports why:

{ "valid": false, "checks": { "notRevoked": false }, "error": "credential revoked" }
{
"valid": false,
"checks": { "notRevoked": false },
"error": "revocation status could not be verified (foreign status list)"
}

The second is the important one. If a credential points at a status list this platform does not host, the result is not valid rather than assumed-good — an unverifiable revocation status is treated as a failure, because the alternative is that anyone can suppress revocation by pointing their credential’s status entry somewhere unreachable.

You do not have to call the endpoint. The credential names its issuer DID in the kid header; resolve the DID document, take the key, and verify:

did:web:acme.oauth.work → https://acme.oauth.work/.well-known/did.json
import { importJWK, jwtVerify, decodeProtectedHeader } from 'jose'
const { kid } = decodeProtectedHeader(credential.split('~')[0])
const doc = await fetch('https://acme.oauth.work/.well-known/did.json').then((r) => r.json())
const method = doc.verificationMethod.find((m) => m.id === kid)
const key = await importJWK(method.publicKeyJwk, 'EdDSA')
const { payload } = await jwtVerify(credential.split('~')[0], key, { algorithms: ['EdDSA'] })

Then check the status list yourself — see revocation. The DID document lists every key the org has published, including retired ones, so credentials signed before a rotation still verify.

A holder presenting a credential with proof of possession — rather than a bearer copy — uses the presentation endpoint:

POST /credentials/presentations/verify

Bind a credential to a holder at issuance with holderDid (a did:key) and only that holder can present it as their own.

Credentials are valid for 90 days by default. Because that is far longer than an access token, retired signing keys stay published and revocation is the mechanism for cutting one short.

SituationUse
Your own API authorizing your own usersAn access token
Proving something to a party that does not trust your issuerA credential
An agent proving what it is, to a third partyAgentIdentityCredential
Proving an agent may act for a userDelegationCredential