Issuance and wallets
There are two ways to get a credential into someone’s hands: issue it directly against a token you already have, or mint an offer the holder’s wallet redeems. Both end at the same signed credential.
Direct issuance
Section titled “Direct issuance”POST /credentials/issueAuthorization: Bearer <access token>Content-Type: application/json
{ "format": "sd-jwt-vc", "holderDid": "did:key:z6Mk…" }{ "vc_id": "vc_…", "type": "WorkCredential", "format": "sd-jwt-vc", "credential": "eyJ…~WyJ…", "issuer": "did:web:acme.oauth.work", "expires_at": 1757776000000}The type is chosen from the token, not from a parameter:
| Token | Credential issued |
|---|---|
A user token with the work_credential scope | WorkCredential |
| An agent or service token | AgentIdentityCredential |
A delegated token (carrying act) | DelegationCredential |
A user token without work_credential gets 403 insufficient_scope. Agent and delegation
credentials do not require that scope — the token’s own nature is the authorization.
holderDid is optional but recommended. Passing a did:key binds the credential to a key the
holder controls, so they can prove possession rather than presenting a bearer document that works
for whoever holds a copy.
Audience binding
Section titled “Audience binding”The token must be valid for this endpoint. A token audience-bound to some other resource is
rejected with 401 invalid_token, “audience mismatch” — so a token minted for an MCP server cannot
be redirected into minting credentials. If the token is DPoP-bound, a matching proof is
required as well.
Issuance for wallets
Section titled “Issuance for wallets”For a holder using a wallet, mint a pre-authorized offer and let them redeem it. Requires
credentials:issue:
POST /v1/organizations/org_01J…/credential_offersAuthorization: Bearer sk_live_…
{ "user_id": "usr_01J…" }{ "credential_offer": { "credential_issuer": "https://acme.oauth.work", "credential_configuration_ids": ["WorkCredential"], "grants": { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": "pac_…" } } }, "pre_authorized_code": "pac_…"}Render credential_offer as a QR code or a deep link. Offers are valid for 10 minutes and are
single-use — redemption is arbitrated in the database, so two wallets racing to redeem the same
offer cannot both succeed.
What the wallet does
Section titled “What the wallet does”1. Discover the issuer.
GET /.well-known/openid-credential-issuer{ "credential_issuer": "https://acme.oauth.work", "credential_endpoint": "https://acme.oauth.work/credential", "credential_configurations_supported": { "WorkCredential": { "format": "vc+sd-jwt", "vct": "WorkCredential", "cryptographic_binding_methods_supported": ["jwk", "did:key"], "credential_signing_alg_values_supported": ["EdDSA"], "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": ["EdDSA", "ES256"] } } } }}2. Redeem the code for a token.
POST /tokenContent-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code&pre-authorized_code=pac_…{ "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600, "scope": "work_credential", "c_nonce": "…", "c_nonce_expires_in": 3600}3. Request the credential, proving possession of the holder key with the c_nonce:
POST /credentialAuthorization: Bearer eyJ…Content-Type: application/json
{ "format": "vc+sd-jwt", "proof": { "proof_type": "jwt", "jwt": "<holder proof signed over c_nonce>" } }The c_nonce binds the holder’s proof to this specific token, so a proof captured from one issuance
cannot be replayed into another.
Choosing between them
Section titled “Choosing between them”Direct issuance suits a service acting for a user it has already authenticated — your backend holds the token, and the credential goes straight into your own storage or UI.
Offers suit a holder with a wallet you do not control. Nothing has to be transmitted to them except a QR code, and the wallet key never leaves their device.
Recording and events
Section titled “Recording and events”Every issued credential is recorded with its type, format, holder, expiry, and
status-list position. Issuance writes a vc_issued audit event and fires a
credential.issued webhook.