Custom Flow
The full-flow endpoint (credentials/receive) runs the whole exchange in one call. The custom flow lets you drive each step yourself when you need to:
- Build a custom wallet UI with consent screens and PIN prompts
- Use the authorization-code grant (browser redirect)
- Handle deferred issuance
- Inspect intermediate results, or manage keys/credentials externally
All endpoints live under /v2/{target}/wallet-service-api/.
Holder Key & DID
Every receive flow signs a proof of possession to prove the wallet controls the holder key. You can let the wallet use its defaults, reference stored material, or pass material inline:
| Field | Type | Description |
|---|---|---|
keyId | String | Full resource path of a key in a linked KMS, e.g. waltid.tenant1.kms.wallet_key. |
key | Object | Inline serialized key ({ "type": "jwk", "jwk": { … } }). Takes precedence over keyId. |
did | String | DID to use as the holder identity, e.g. did:jwk:…. Resolved against a linked DID Store or used inline. |
All three are optional — if omitted, the wallet uses its configured default key and DID.
Pre-Authorized Code Flow
Drive the pre-authorized code grant one step at a time:
1. Resolve Offer → 2. Request Token → 2b. Get Nonce → 3. Sign Proof → 4. Fetch Credential
For the authorization-code grant (browser redirect, user login), see Authorization-Code Grant below — it reuses Get Nonce, Sign Proof, and Fetch Credential from this flow.
Step 1 — Resolve Offer
Parse the offer URL and resolve the issuer metadata in one call.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/resolve-offer
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/resolve-offer' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2Fcredential-offer%3Fid%3Dabc123"
}'
Body Parameters
- Credential offer (required) — provide exactly one of:
offerUrlString — The OID4VCI offer URL.offerJsonObject — Raw credential offer JSON, instead of a URL.
Example Response
{
"credentialIssuer": "https://issuer.example.org",
"credentialConfigurationIds": ["UniversityDegree_jwt_vc_json"],
"grantType": "pre-authorized_code",
"txCodeRequired": false,
"credentialEndpoint": "https://issuer.example.org/credential",
"offeredCredentials": ["UniversityDegree_jwt_vc_json"]
}
grantType—pre-authorized_codeorauthorization_code.txCodeRequired— Whether a transaction code (PIN) is required for the pre-authorized grant.credentialEndpoint— Where to POST the credential request in Step 4.
Response Codes
200— Offer resolved.400— Offer could not be resolved.
Step 2 — Request Token
Exchange a 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 /v2/{target}/wallet-service-api/credentials/receive/request-token
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/request-token' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "https://auth.example.org/token",
"preAuthorizedCode": "SplxlOBeZQQYbYS6WxSbIA",
"txCode": "493536"
}'
Body Parameters
tokenEndpointString — The authorization server's token endpoint.preAuthorizedCodeString — The pre-authorized code from the offer.txCode(optional) String — Transaction code (PIN), if the offer requires one.clientId(optional) String — Defaults towallet-client.redirectUri(optional) String — Defaults toopenid://.
Example Response
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 300
}
accessToken— Bearer token for the credential request in Step 4.cNonce(optional) — Proof nonce, if the issuer returns one in the token response. Under OID4VCI 1.0 the issuer (including Issuer2) instead exposes a dedicated Nonce Endpoint, socNonceis usually absent here — fetch the nonce in Step 2b instead.
Response Codes
200— Token issued.400— Invalid or expired pre-authorized code.
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 Step 2b before signing the proof.
Step 2b — 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 this — 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 from Step 1).
Endpoint: POST {credentialIssuer}/nonce — this is the issuer's endpoint, not a wallet endpoint.
Example Request
curl -X POST 'https://issuer.example.org/nonce'
Example Response
{
"c_nonce": "wKI4LT5Sadf6a0dyih5b0Ws"
}
Use the returned c_nonce as the nonce in Step 3.
Response Codes
200— Nonce issued.
Step 3 — Sign Proof
Sign a proof of possession over the issuer's c_nonce to prove control of the holder key.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/sign-proof
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/sign-proof' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"issuerUrl": "https://issuer.example.org",
"nonce": "wKI4LT5Sadf6a0dyih5b0Ws",
"keyId": "waltid.tenant1.kms.wallet_key",
"did": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Ii..."
}'
Body Parameters
issuerUrlString — The credential issuer identifier (used as the proofaud).nonceString — Thec_noncefrom the Nonce Endpoint (Step 2b), or from the token response if the issuer returned one.keyId/key(optional) — Holder key reference or inline key (see Holder Key & DID).did(optional) String — Holder DID. When set, the proof binds to the DID; otherwise it binds to the raw JWK.
Example Response
{
"proofJwt": "eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dC..."
}
Response Codes
200— Proof signed.400— No resolvable holder key.
Step 4 — Fetch Credential
Request the credential from the issuer's credential endpoint using the access token and the signed proof.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/fetch-credential
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/fetch-credential' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"credentialEndpoint": "https://issuer.example.org/credential",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"proofJwt": "eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dC..."
}'
Body Parameters
credentialEndpointString — The issuer's credential endpoint (from Step 1 or issuer metadata).accessTokenString — The access token from Step 2.credentialConfigurationIdString — The credential configuration being requested.proofJwt(optional) String — The proof JWT from Step 3. Required when the issuer mandates key-proof.clientId(optional) String — Defaults towallet-client.
Example Response
{
"rawCredentials": [
"eyJraWQiOiJkaWQ6a2V5Ono2TWtqb1Jo..."
]
}
rawCredentials— The issued credential string(s) (JWT, SD-JWT, or base64-encoded mDoc). These are not stored automatically; use the full-flow endpoints if you want the wallet to persist them.
Response Codes
200— Credential(s) fetched.400/500— The issuer rejected the credential request (e.g. missing or invalid proof).
Authorization-Code Grant
You don't know which grant an offer uses until you resolve it — Step 1 — Resolve Offer above returns grantType, and authorization_code is what routes you here instead of the pre-authorized flow. The authorization-code grant then requires the user to authenticate with the issuer's authorization server via a browser redirect, so it cannot be a single blocking call:
Step 1 (above) — Resolve Offer → grantType: "authorization_code"
Generate Authorization URL → redirect the user to the issuer
── user authenticates in the browser ──
Complete Receive → exchange the code, sign the proof, fetch & store the credential (single call)
— or drive it yourself: —
Exchange Code → Step 2b (above) — Get Nonce → Step 3 (above) — Sign Proof → Step 4 (above) — Fetch Credential
(Get Nonce, Sign Proof, and Fetch Credential are the same steps used by the pre-authorized flow above)
Generate Authorization URL
Resolve the offer and build the authorization URL the user must be redirected to.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/authorization-url
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/authorization-url' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2Fcredential-offer%3Fid%3Dabc123",
"redirectUri": "https://your-wallet-app.example.com/callback"
}'
Body Parameters
- Credential offer (required) — provide exactly one of:
offerUrlString — The OID4VCI offer URL.offerJsonObject — Raw credential offer JSON, instead of a URL.
clientId(optional) String — Defaults towallet-client.redirectUri(optional) String — Where the authorization server redirects the user after authentication. Defaults toopenid://.usePkce(optional) Boolean — Whether to use PKCE. Defaults totrue.
Example Response
{
"authorizationUrl": "https://auth.example.org/authorize?response_type=code&client_id=wallet-client&redirect_uri=https%3A%2F%2Fyour-wallet-app.example.com%2Fcallback&scope=UniversityDegree_jwt_vc_json&issuer_state=eyJhbGciOiJSU...&code_challenge=abc123&code_challenge_method=S256",
"state": "abc123",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json"
}
authorizationUrl— Redirect the user here to begin authentication.state— Returned to yourredirectUrias a query parameter; use it to correlate the callback.codeVerifier— PKCE code verifier. Store this securely — the Exchange Code step below needs it.credentialConfigurationId— The credential configuration this authorization is for.
After redirecting the user, your app receives the authorization code at redirectUri: ?code=<authorization_code>&state=<state>.
Response Codes
200— Authorization URL generated.400— Offer could not be resolved, or the issuer has no authorization endpoint.
Exchange Code (Isolated)
Exchange the authorization code obtained above for an access token. This is the isolated alternative to Complete Receive below — after this, continue with Step 2b — Get a Nonce, Step 3 — Sign Proof, and Step 4 — Fetch Credential from the pre-authorized flow above; they're identical for both grant types.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/exchange-code
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/exchange-code' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "https://auth.example.org/token",
"code": "SplxlOBeZQQYbYS6WxSbIA",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
}'
Body Parameters
tokenEndpointString — The authorization server's token endpoint.codeString — The authorization code from the redirect callback.codeVerifier(optional) String — The PKCE code verifier saved from the authorization-url step.clientId(optional) String — Defaults towallet-client.redirectUri(optional) String — Must match the redirect URI used earlier. Defaults toopenid://.
Example Response
Same structure as Step 2 — Request Token in the pre-authorized flow.
Response Codes
200— Token issued.400— Invalid authorization code or PKCE verifier.
Complete Receive (Combined Alternative)
The simpler alternative to driving Exchange Code, Get Nonce, Sign Proof, and Fetch Credential yourself once the user is back with an authorization code: exchange the code for a token, sign the proof, fetch the credential, and store it — in one call.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/authorized
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/authorized' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"tokenEndpoint": "https://auth.example.org/token",
"code": "SplxlOBeZQQYbYS6WxSbIA",
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"credentialIssuer": "https://issuer.example.org",
"credentialEndpoint": "https://issuer.example.org/credential",
"nonceEndpoint": "https://issuer.example.org/nonce",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"keyId": "waltid.tenant1.kms.wallet_key",
"did": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Ii..."
}'
Body Parameters
tokenEndpointString — The authorization server's token endpoint (from its metadata).codeString — The authorization code received at yourredirectUri.codeVerifier(optional) String — The PKCE code verifier saved from Step 1. Required if PKCE was used.credentialIssuerString — The credential issuer identifier.credentialEndpointString — The issuer's credential endpoint (from its metadata).credentialConfigurationIdString — The credential configuration being requested.nonceEndpoint(optional in the request schema, but effectively required for Issuer2) String — The issuer's nonce endpoint — for Issuer2 this is{credentialIssuer}/nonce. Under OID4VCI 1.0 the token response typically carries noc_nonce(same as Step 2b), so if you omitnonceEndpointand the issuer doesn't return ac_nonce, this call fails with a500.keyId/key(optional) — Holder key reference or inline key.did(optional) String — Holder DID.clientId(optional) String — Defaults towallet-client.redirectUri(optional) String — Must match theredirectUriused in Step 1. Defaults toopenid://.
Example Response
{
"credentialIds": [
"f58ac347-0728-45c6-ba59-bde9d3343aa6"
],
"deferredTransactionIds": {}
}
Response Codes
200— Credential(s) received and stored.400— Invalid authorization code, or the issuer rejected the request.500— The issuer provided noc_nonceand nononceEndpointwas supplied to fetch one.
Deferred Issuance
If the issuer defers a credential, the full-flow (or authorized) response returns its transaction_id under deferredTransactionIds. Poll the issuer's deferred credential endpoint until the credential becomes available.
Endpoint: POST /v2/{target}/wallet-service-api/credentials/receive/deferred
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/deferred' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"deferredCredentialEndpoint": "https://issuer.example.org/credential_deferred",
"transactionId": "8xLOxBtZp8",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
Body Parameters
deferredCredentialEndpointString — The deferred credential endpoint from the issuer's metadata.transactionIdString — Thetransaction_idreturned underdeferredTransactionIds.accessTokenString — The access token from the original token response.
Example Response
{
"credentialIds": [
"f58ac347-0728-45c6-ba59-bde9d3343aa6"
],
"deferredTransactionIds": {}
}
If the credential is not yet ready, credentialIds is empty — retry later.
Response Codes
200— Poll completed (credential stored, or not yet ready).400/500— The issuer rejected the deferred request.
Next Steps
- Receive with the full flow — the per-format guides (SD-JWT VC, W3C VC, mDL) use the single-call
credentials/receiveendpoint. - Manage stored credentials — list and retrieve credentials in the wallet.
- Present a credential — Credential Presenting.
