Custom Flow
The full-flow endpoint runs the whole OID4VCI exchange in a single call. When you're building a wallet UI and need to show intermediate state, prompt for a PIN, support the authorization-code grant, or handle deferred issuance, drive the isolated endpoints explained on this page yourself.
All endpoints on this page are rooted at /wallet/{walletId}/credentials/receive. The examples use http://localhost:7005 as the base URL.
When to Use the Isolated Flow
| You want to… | Use |
|---|---|
| Claim a credential in one call | Full flow |
| Preview the offer before claiming | Resolve the offer first |
| Issuer requires user auth before claiming | Authorization-code flow |
| Poll for a credential issued later | Deferred credentials |
| Manage key material outside the wallet | Inline keys |
Pre-Authorized Code Flow
The isolated pre-authorized flow is:
1. Resolve Offer → 2. Request Token → 2b. Get Nonce → 3. Sign Proof → 4. Fetch Credential
Resolve the Offer
Parse a credential offer and fetch the issuer metadata needed for the later steps.
Endpoint: POST /wallet/{walletId}/credentials/receive/resolve-offer | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/resolve-offer \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Flocalhost%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}'
Path Parameters
- walletId: String (required) - The wallet handling the exchange. See how to obtain it.
Body Parameters
- Credential offer (required) — provide exactly one of:
- offerUrl: String - The OID4VCI credential offer URL.
- offerJson: Object - The credential offer as an inline JSON object.
Example Response
{
"credentialIssuer": "http://localhost:7005/openid4vci",
"credentialConfigurationIds": ["identity_credential"],
"grantType": "pre-authorized_code",
"txCodeRequired": true,
"credentialEndpoint": "http://localhost:7005/openid4vci/credential",
"offeredCredentials": ["identity_credential"]
}
Response Fields
- credentialIssuer: String - Issuer identifier from the offer.
- credentialConfigurationIds: String - Credential configuration IDs listed in the offer.
- grantType: String -
pre-authorized_codeorauthorization_code. - txCodeRequired: Boolean - Whether the pre-authorized flow requires a transaction code (PIN).
- credentialEndpoint: String - The issuer's credential endpoint, from its metadata.
- offeredCredentials: String - Resolved offered credential configuration IDs.
Request Token
Exchange the pre-authorized code for an access token.
resolve-offer does not return the preAuthorizedCode or tokenEndpoint this step needs — source them yourself. The pre-authorized code is carried inside the credential offer at grants."urn:ietf:params:oauth:grant-type:pre-authorized_code"."pre-authorized_code". The token endpoint is the issuer's token_endpoint from its authorization-server metadata — for Issuer2 that is {credentialIssuer}/token (the credentialIssuer returned by resolve-offer).
Endpoint: POST /wallet/{walletId}/credentials/receive/request-token | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/request-token \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "http://localhost:7005/openid4vci/token",
"preAuthorizedCode": "SplxlOBeZQQYbYS6WxSbIA",
"txCode": "123456"
}'
Body Parameters
- tokenEndpoint: String (required) - The issuer's token endpoint.
- preAuthorizedCode: String (required) - The pre-authorized code from the credential offer.
- txCode: String (optional) - Transaction code (PIN), if the offer requires one.
- clientId: String (optional) - See OAuth client identity. Defaults to
wallet-client. - redirectUri: String (optional) - Defaults to
openid://.
Example Response
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 300
}
Response Fields
- accessToken: String - Use it in the fetch-credential request.
- cNonce: String (optional) - Proof nonce, only if the issuer returns one in the token response. Under OID4VCI 1.0 the issuer (including Issuer2) serves the nonce from a dedicated Nonce Endpoint instead, so this is usually absent — fetch it in Get a Nonce below.
- expiresIn: Number - Token lifetime in seconds.
Under OID4VCI 1.0 the proof nonce (c_nonce) comes from the issuer's Nonce Endpoint, not the token response — so request-token typically returns no cNonce. Get it via Get a Nonce before signing the proof.
Get a Nonce
Under OID4VCI 1.0 the proof c_nonce comes from the issuer's dedicated Nonce Endpoint, not the token response. The wallet does not proxy it — call the issuer's nonce endpoint directly. Its URL is the nonce_endpoint published in the issuer's credential-issuer metadata; for Issuer2 it is {credentialIssuer}/nonce (the credentialIssuer returned by resolve-offer).
Endpoint: POST {credentialIssuer}/nonce — the issuer's endpoint, not a wallet endpoint.
Example Request
curl -X POST http://localhost:7005/openid4vci/nonce
Example Response
{
"c_nonce": "wKI4LT5Sadf6a0dyih5b0Ws"
}
Use the returned c_nonce as the nonce in Sign Proof.
Sign Proof
Sign a proof-of-possession JWT that proves the wallet controls the holder key.
Endpoint: POST /wallet/{walletId}/credentials/receive/sign-proof | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/sign-proof \
-H 'Content-Type: application/json' \
-d '{
"issuerUrl": "http://localhost:7005/openid4vci",
"nonce": "wKI4LT5Sadf6a0dyih5b0Ws"
}'
Body Parameters
- issuerUrl: String (required) - The credential issuer URL, used as the proof audience.
- nonce: String (required) - The
c_noncefrom Get a Nonce (or from the token response, if the issuer returned one). - keyId: String (optional) - Key from the wallet's key store. Defaults to the wallet's default key.
- did: String (optional) - DID to use as the proof key ID. If omitted, the proof carries the JWK directly.
- key: Object (optional) - An inline key. Takes precedence over
keyId.
Example Response
{
"proofJwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0In0..."
}
Fetch Credential
Request the credential from the issuer's credential endpoint.
Endpoint: POST /wallet/{walletId}/credentials/receive/fetch-credential | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/fetch-credential \
-H 'Content-Type: application/json' \
-d '{
"credentialEndpoint": "http://localhost:7005/openid4vci/credential",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"credentialConfigurationId": "identity_credential",
"proofJwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6Im9wZW5pZDR2Y2ktcHJvb2Yrand0In0..."
}'
Body Parameters
- credentialEndpoint: String (required) - The issuer's credential endpoint.
- accessToken: String (required) - The access token from the token response.
- credentialConfigurationId: String (required) - The credential configuration ID to request.
- proofJwt: String (optional) - The proof JWT from the sign-proof step. Required if the issuer requires proof.
- clientId: String (optional) - See OAuth client identity. Defaults to
wallet-client.
Example Response
{
"rawCredentials": [
"eyJraWQiOiJkaWQ6a2V5Ono2TWtmcmlxMU1xTEJvUFdlY0dvRExqZ3VvMXNCOWJyajZ3VDNxWjVCeGtLcHVQNiJ9..."
]
}
The isolated fetch endpoint returns raw credential strings — it does not store them. To keep a credential, import it with POST /wallet/{walletId}/credentials/import, or use the full flow, which stores automatically.
Authorization-Code Flow
When the issuer requires the holder to log in first, use the authorization-code grant. It replaces the "request token" step with a redirect through the issuer's authorization server.
1. Resolve Offer → 2. Generate Authorization URL → (user logs in, issuer redirects back)
→ 3. Exchange Code → 4. Sign Proof → 5. Fetch Credential
Steps 1, 4 and 5 are identical to the pre-authorized flow above.
Generate Authorization URL
Build the authorization URL to redirect the user to.
Endpoint: POST /wallet/{walletId}/credentials/receive/authorization-url | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/authorization-url \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=...",
"redirectUri": "https://wallet.example.org/callback",
"usePkce": true
}'
Body Parameters
- Credential offer (required) — provide exactly one of:
- offerUrl: String - The OID4VCI credential offer URL.
- offerJson: Object - The credential offer as an inline JSON object.
- redirectUri: String (optional) - Redirect URI registered with the issuer. Defaults to
openid://. - usePkce: Boolean (optional) - Whether to use PKCE. Defaults to
true. - clientId: String (optional) - See OAuth client identity. Defaults to
wallet-client.
Example Response
{
"authorizationUrl": "http://localhost:7005/openid4vci/authorize?response_type=code&client_id=wallet-client&state=abc123&code_challenge=...",
"state": "abc123",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"credentialConfigurationId": "identity_credential"
}
Response Fields
- authorizationUrl: String - Redirect the user here.
- state: String - Returned to your redirect URI; use it to correlate the response.
- codeVerifier: String - Store it — you'll need it to exchange the code (PKCE).
- credentialConfigurationId: String - The credential being requested.
After the user authenticates, the issuer redirects to your redirectUri with code and state query parameters.
Exchange Code
Exchange the authorization code for an access token.
Endpoint: POST /wallet/{walletId}/credentials/receive/exchange-code | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/exchange-code \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "http://localhost:7005/openid4vci/token",
"code": "SplxlOBeZQQYbYS6WxSbIA",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"redirectUri": "https://wallet.example.org/callback"
}'
Body Parameters
- tokenEndpoint: String (required) - The issuer's token endpoint.
- code: String (required) - The authorization code returned to your redirect URI.
- codeVerifier: String (optional) - The PKCE code verifier from the authorization-url step.
- redirectUri: String (optional) - The redirect URI used in the authorization request. Defaults to
openid://. - clientId: String (optional) - See OAuth client identity. Defaults to
wallet-client.
Example Response
Returns the same shape as Request Token — an accessToken, optional cNonce, and expiresIn. Continue with Sign Proof and Fetch Credential.
Deferred Credentials
If the issuer isn't ready to issue immediately, the receive result returns a transaction_id in deferredTransactionIds instead of a credential. Poll the deferred endpoint with that ID until the credential is ready.
Endpoint: POST /wallet/{walletId}/credentials/receive/deferred | API Reference
Example Request
curl -X POST http://localhost:7005/wallet/{walletId}/credentials/receive/deferred \
-H 'Content-Type: application/json' \
-d '{
"deferredCredentialEndpoint": "http://localhost:7005/openid4vci/credential_deferred",
"transactionId": "8xFQ5j9Q8L-3PUdnl4RWxXgB7oNc3mK2pL9vR6tY1zA",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
Body Parameters
- deferredCredentialEndpoint: String (required) - The deferred credential endpoint from the issuer metadata.
- transactionId: String (required) - The transaction ID returned when issuance was deferred.
- accessToken: String (required) - The access token from the original token response.
Example Response
{
"credentialIds": ["550e8400-e29b-41d4-a716-446655440000"],
"deferredTransactionIds": {}
}
If the issuer is still not ready, credentialIds comes back empty — retry later.
Advanced Options
OAuth Client Identity
Several endpoints accept a clientId. It is the OAuth 2.0 client_id the wallet presents to the issuer's authorization server in token and authorization requests (and it feeds client-attestation headers when those are used).
The default, wallet-client, works with issuers that don't require client registration — including the walt.id Issuer2 in its default configuration. Set it explicitly when the issuer's authorization server requires a registered or attested client identity.
Inline Keys
The sign-proof step (and the full-flow receive) accept an inline key object instead of a keyId. An inline key takes precedence over keyId and the wallet's default key. Use it only when the caller manages key material outside the wallet store — most deployments should prefer keyId or the default key.
{
"key": {
"type": "jwk",
"jwk": { "kty": "OKP", "crv": "Ed25519", "x": "...", "d": "..." }
}
}
The key uses the same serialized shape as the key import endpoint.
Token Request Headers
ReceiveCredentialRequest includes a tokenRequestHeaders map, intended for headers such as OAuth-Client-Attestation and OAuth-Client-Attestation-PoP.
The current token request builder does not forward these per-request headers. If tokenRequestHeaders are supplied, the service logs a warning and continues without sending them. Do not rely on this field for attestation-based client authentication until the underlying token builder supports additional headers.
Endpoint Reference
| Endpoint | Method | Description |
|---|---|---|
/wallet/{walletId}/credentials/receive | POST | Full pre-authorized code flow |
/wallet/{walletId}/credentials/receive/resolve-offer | POST | Resolve a credential offer |
/wallet/{walletId}/credentials/receive/request-token | POST | Exchange a pre-authorized code for a token |
/wallet/{walletId}/credentials/receive/authorization-url | POST | Generate an authorization-code redirect URL |
/wallet/{walletId}/credentials/receive/exchange-code | POST | Exchange an authorization code for a token |
/wallet/{walletId}/credentials/receive/sign-proof | POST | Sign a proof-of-possession JWT |
/wallet/{walletId}/credentials/receive/fetch-credential | POST | Fetch credential(s) from the issuer |
/wallet/{walletId}/credentials/receive/deferred | POST | Poll deferred credential issuance |
