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:

FieldTypeDescription
keyIdStringFull resource path of a key in a linked KMS, e.g. waltid.tenant1.kms.wallet_key.
keyObjectInline serialized key ({ "type": "jwk", "jwk": { … } }). Takes precedence over keyId.
didStringDID 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.

CURL

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:
    • offerUrl String — The OID4VCI offer URL.
    • offerJson Object — 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"]
}
  • grantTypepre-authorized_code or authorization_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).

CURL

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

  • tokenEndpoint String — The authorization server's token endpoint.
  • preAuthorizedCode String — The pre-authorized code from the offer.
  • txCode (optional) String — Transaction code (PIN), if the offer requires one.
  • clientId (optional) String — Defaults to wallet-client.
  • redirectUri (optional) String — Defaults to openid://.

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, so cNonce is 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).

CURL

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.

CURL

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

  • issuerUrl String — The credential issuer identifier (used as the proof aud).
  • nonce String — The c_nonce from 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.

CURL

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

  • credentialEndpoint String — The issuer's credential endpoint (from Step 1 or issuer metadata).
  • accessToken String — The access token from Step 2.
  • credentialConfigurationId String — 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 to wallet-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.

CURL

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:
    • offerUrl String — The OID4VCI offer URL.
    • offerJson Object — Raw credential offer JSON, instead of a URL.
  • clientId (optional) String — Defaults to wallet-client.
  • redirectUri (optional) String — Where the authorization server redirects the user after authentication. Defaults to openid://.
  • usePkce (optional) Boolean — Whether to use PKCE. Defaults to true.

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 your redirectUri as 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.

CURL

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

  • tokenEndpoint String — The authorization server's token endpoint.
  • code String — The authorization code from the redirect callback.
  • codeVerifier (optional) String — The PKCE code verifier saved from the authorization-url step.
  • clientId (optional) String — Defaults to wallet-client.
  • redirectUri (optional) String — Must match the redirect URI used earlier. Defaults to openid://.

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.

CURL

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

  • tokenEndpoint String — The authorization server's token endpoint (from its metadata).
  • code String — The authorization code received at your redirectUri.
  • codeVerifier (optional) String — The PKCE code verifier saved from Step 1. Required if PKCE was used.
  • credentialIssuer String — The credential issuer identifier.
  • credentialEndpoint String — The issuer's credential endpoint (from its metadata).
  • credentialConfigurationId String — 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 no c_nonce (same as Step 2b), so if you omit nonceEndpoint and the issuer doesn't return a c_nonce, this call fails with a 500.
  • keyId / key (optional) — Holder key reference or inline key.
  • did (optional) String — Holder DID.
  • clientId (optional) String — Defaults to wallet-client.
  • redirectUri (optional) String — Must match the redirectUri used in Step 1. Defaults to openid://.

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 no c_nonce and no nonceEndpoint was 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.

CURL

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

  • deferredCredentialEndpoint String — The deferred credential endpoint from the issuer's metadata.
  • transactionId String — The transaction_id returned under deferredTransactionIds.
  • accessToken String — 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

Last updated on July 13, 2026