Receive Credentials via OID4VCI 1.0
The credential receive v2 endpoints are a modernized set of wallet APIs that support the full OID4VCI pre-authorized and authorization-code flows.
They live under wallet-service-api and offer two usage modes:
| Mode | When to use |
|---|---|
| Full-flow (single call) | You want the wallet to handle the entire OID4VCI exchange in one request. |
| Isolated step-by-step | You need visibility into each intermediate result — e.g., for a custom wallet UI, user-consent screens, or multi-step orchestration. |
Ensure you have set up a wallet service with a linked KMS and (optionally) a Credential Store before proceeding. If no Credential Store is linked the received credential will not be persisted automatically.
Base URL
All v2 wallet receive endpoints follow this pattern:
/v2/{organization}.{tenant}.{wallet-id}/wallet-service-api/<path>
organization— your organization IDtenant— the tenant the wallet belongs towallet-id— the ID of the wallet service instance
For example:
/v2/waltid.tenant1.wallet/wallet-service-api/credentials/receive/pre-authorized
Credential Offer URL
A credential offer URL is a standardized method, as per the OID4VCI specification, to communicate the issuance of credentials between issuer and wallet. This URL can take various forms, such as a QR code or a link, and generally begins with openid-credential-offer://.
Example Offer URL
openid-credential-offer://issuer.portal.walt.id/?credential_offer=<credential_offer>
Example of a Credential Offer Object
{
"credential_issuer": "https://issuer.demo.walt.id",
"credential_configuration_ids": [
"identity_credential_vc+sd-jwt"
],
"grants": {
"authorization_code": {
"issuer_state": "<issuer_state>"
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "<pre-authorized_code>",
"user_pin_required": false
}
}
}
Pre-Authorized Flow
The pre-authorized flow is used when the issuer provides a pre-authorized_code in the credential offer. This flow is typically used for automated issuance or when the user has already authenticated with the issuer via another channel.
Full-Flow (Single Call)
Executes the entire pre-authorized code flow: resolve offer → fetch issuer metadata → request token → request credential → store credential.
Endpoint: /v2/{organization}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/pre-authorized | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credentials/receive/pre-authorized' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://issuer.example.org/issue/?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2FcredentialOffer%3Fabc123xyz789",
"keyReference": "org.tenant.wallet-id.key-id"
}'
Body Parameters — WalletReceiveRequest
offerUrl(optional*) String — OID4VCI credential offer URL (openid-credential-offer://...). Use this oroffer.offer(optional*) Object — RawCredentialOfferJSON object. Alternative toofferUrl.keyReference(optional*) String — Target reference (org.tenant.wallet.key-id) of a key stored in a linked KMS.key(optional*) Object — Inline key object (JWK, TSE, AWS, OCI). Alternative tokeyReference.didReference(optional) String — Target reference of a DID stored in a linked DID Store.did(optional) String — Inline DID string. Alternative todidReference.txCode(optional) String — Transaction (PIN) code required by the issuer for pre-authorized flow.runPolicies(optional) Boolean — Whentrue, applies any configured holder policies before storing.nullmeans policies run only if a match exists;falseskips policies entirely.useClientAttestation(optional) Boolean — Whentrue, loads the storedWalletClientAttestationand sends it in the token request. Requires a client attestation to have been previously obtained.metadata(optional) Object — Arbitrary JSON stored alongside the credential in the Credential Store.trustedVicals(optional) Array — Whitelist of trusted VICAL artifacts for mDoc IACA certificate chain validation.
Response Codes
201— Credential(s) received successfully.
Body — List<ReceiveResponse>
[
{
"stored": "urn:uuid:224bcbc4-7436-4854-8ef1-cc6e3b8236bd",
"credential": "eyJraWQiOiJ..."
}
]
stored— The credential ID assigned in the Credential Store, ornullif no store is linked.credential— The raw issued credential string (JWT, SD-JWT, or base64-encoded mDoc).
Authorized Flow (Authorization Code)
The authorized flow is used when the user must authenticate with the issuer via an external authorization server (e.g., via a browser redirect). Because it involves a user redirect, the exchange is performed in steps.
Flow Overview
1. Resolve Offer → parse offer URL → CredentialOffer
2. Fetch Metadata → CredentialOffer → CredentialIssuerMetadata
3. View Offered Creds → ResolvedOfferWithMetadata → List<OfferedCredential>
4. Generate Auth URL → build authorization URL → redirect user to browser
── user authenticates in browser ──
5. Exchange Code → authorization_code → TokenResponse (access_token)
6. Receive Credential → access_token + key → stored credential
Step 1 — Resolve Credential Offer
Parse the raw offer URL into a structured CredentialOffer object.
Required permission: ISOLATED_RESOLVE_OFFER
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/offer/resolve | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/offer/resolve' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: text/plain' \
-d 'openid-credential-offer://issuer.example.org/issue/?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2FcredentialOffer%3Fabc123xyz789'
The request body is the raw offer URL as a plain string.
Response — CredentialOffer
{
"credential_issuer": "https://issuer.example.org",
"credential_configuration_ids": [
"identity_credential_vc+sd-jwt"
],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU..."
}
}
}
Save the full CredentialOffer object — it is used as input for Steps 2, 3, and 4.
Step 2 — Fetch Issuer Metadata
Retrieve the issuer's CredentialIssuerMetadata, which describes supported credential formats, cryptographic binding methods, and display information.
Required permission: ISOLATED_PROVIDER_METADATA
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/metadata/view | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/metadata/view' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"credential_issuer": "https://issuer.example.org",
"credential_configuration_ids": ["identity_credential_vc+sd-jwt"],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU..."
}
}
}'
The request body is the CredentialOffer object returned from Step 1.
Response — CredentialIssuerMetadata
{
"credential_issuer": "https://issuer.example.org",
"authorization_servers": ["https://auth.example.org"],
"credential_endpoint": "https://issuer.example.org/credential",
"credential_configurations_supported": {
"identity_credential_vc+sd-jwt": {
"format": "vc+sd-jwt",
"vct": "https://issuer.example.org/identity_credential",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256", "EdDSA"],
"display": [
{
"name": "Identity Credential",
"locale": "en-US"
}
]
}
}
}
Step 3 — View Offered Credentials
List the credentials being offered, with full display and format details. Use this step to show the user a consent screen.
Required permission: ISOLATED_OFFER_CREDENTIALS
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/offer/credentials/view | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/offer/credentials/view' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"offer": {
"credential_issuer": "https://issuer.example.org",
"credential_configuration_ids": ["identity_credential_vc+sd-jwt"],
"grants": {
"authorization_code": {
"issuer_state": "eyJhbGciOiJSU..."
}
}
},
"metadata": {
"credential_issuer": "https://issuer.example.org",
"credential_configurations_supported": {
"identity_credential_vc+sd-jwt": {
"format": "vc+sd-jwt",
"vct": "https://issuer.example.org/identity_credential",
"cryptographic_binding_methods_supported": ["jwk"],
"display": [{ "name": "Identity Credential", "locale": "en-US" }]
}
}
}
}'
The request body is a ResolvedOfferWithMetadata object combining the CredentialOffer (Step 1) and the CredentialIssuerMetadata (Step 2).
Response — List<OfferedCredential>
[
{
"id": "identity_credential_vc+sd-jwt",
"format": "vc+sd-jwt",
"vct": "https://issuer.example.org/identity_credential",
"cryptographic_binding_methods_supported": ["jwk"],
"display": [
{
"name": "Identity Credential",
"locale": "en-US",
"background_color": "#12107c",
"text_color": "#FFFFFF"
}
]
}
]
Step 4 — Generate Authorization URL
Build the URL to which the user must be redirected to authenticate with the issuer's authorization server.
Required permission: ISOLATED_TOKEN_SEND_REQUEST
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/authorization-url/generate | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/authorization-url/generate' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"offer": "openid-credential-offer://issuer.example.org/issue/?credential_offer_uri=https%3A%2F%2Fissuer.example.org%2FcredentialOffer%3Fabc123xyz789",
"redirectUri": "https://your-wallet-app.example.com/callback"
}'
Body Parameters — ResolveAuthorizationUrlRequestV2
offerString — The original OID4VCI offer URL.redirectUriString — The URI to which the authorization server will redirect the user after authentication. Must match a registered redirect URI for your client.
Response — Authorization URL
{
"url": "https://auth.example.org/authorize?response_type=code&client_id=your-wallet-client-id&redirect_uri=https%3A%2F%2Fyour-wallet-app.example.com%2Fcallback&scope=identity_credential_vc%2Bsd-jwt&issuer_state=eyJhbGciOiJSU...&code_challenge=abc123&code_challenge_method=S256",
"state": "abc123",
"pkceData": {
"codeVerifier": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"codeChallengeMethod": "S256"
}
}
url— Redirect the user to this URL to begin authentication.codeVerifier— PKCE code verifier. Store this securely — you need it in Step 5 to exchange the code.
After redirecting the user, your app receives an authorization code at redirectUri as a query parameter: ?code=<authorization_code>&state=<state>.
Step 5 — Exchange Authorization Code for Token
Exchange the authorization code received from the redirect for an access token.
Required permission: ISOLATED_TOKEN_SEND_REQUEST
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/token-request/exchange-code | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/isolated/token-request/exchange-code' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"credential_issuer": "https://issuer.example.org",
"credential_endpoint": "https://issuer.example.org/credential",
"credential_configurations_supported": {
"NaturalPersonVerifiableID_jwt_vc": {
"format": "jwt_vc_json",
"credential_definition": {
"type": [
"VerifiableCredential",
"VerifiableAttestation",
"NaturalPersonVerifiableID"
]
},
"credential_signing_alg_values_supported": [
"ES256",
"ES256K"
],
"cryptographic_binding_methods_supported": [
"jwk"
],
"proof_types_supported": {
"jwt": {
"proof_signing_alg_values_supported": [
"ES256",
"ES256K",
"EdDSA"
]
}
}
}
}
},
"code": "SplxlOBeZQQYbYS6WxSbIA",
"pkceVerifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"redirectUri": "http://localhost:3000/isolated/credentials/receive"
}'
Body Parameters — ExchangeTokenWithAuthorizationCodeRequest
metadataObject — TheCredentialIssuerMetadataobtained in Step 2. This is required to determine the correct token endpoint and client authentication method for the token request.redirectUriString — Must exactly match theredirectUriused in Step 4.codeString — The authorization code received from the redirect callback.codeVerifierString — The PKCE code verifier saved from Step 4.
Response — TokenResponse
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 300,
"c_nonce": "tZignsnFbp",
"c_nonce_expires_in": 86400
}
access_token— Pass this to Step 6 to request the credential.c_nonce— Issuer-provided nonce. The wallet must sign a proof of possession using this nonce when requesting the credential.
Step 6 — Receive Credential
Request the actual credential from the issuer using the access token obtained in Step 5.
Required permission: ISOLATED_RECEIVE_CREDENTIAL
Endpoint: POST /v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credential/receive/authorized | API Reference
Example Request
curl -X POST \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{org}.{tenant}.{wallet-id}/wallet-service-api/credential/receive/authorized' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"credentialEndpoint": "https://issuer.example.org/credential",
"tokenResponse": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"token_type": "Bearer",
"c_nonce": "8xFQ5j9Q8L-3PUdnl4RWxXgB7oNc3mK2pL9vR6tY1zA="
},
"credentialConfigurationId": "NaturalPersonVerifiableID_jwt_vc",
"didReference": "example.didstore.wallet_did",
"keyReference": "example.kms.wallet_key",
"metadata": {
"credential_issuer": "https://issuer.example.org",
"credential_endpoint": "https://issuer.example.org/credential",
"credential_configurations_supported": {
"NaturalPersonVerifiableID_jwt_vc": {
"format": "jwt_vc_json",
"credential_definition": {
"type": [
"VerifiableCredential",
"VerifiableAttestation",
"NaturalPersonVerifiableID"
]
}
}
}
}
}'
Body Parameters — ReceiveCredentialNewRequest
credentialEndpointString — The issuer's credential endpoint URL, obtained from theCredentialIssuerMetadata.tokenResponseObject — The fullTokenResponseobtained in Step 5. The wallet needs theaccess_tokento request the credential and thec_nonceto generate the required proof of possession.credentialConfigurationIdString — The ID of the credential configuration being requested, as listed in theCredentialOfferandCredentialIssuerMetadata. This tells the wallet which credential format to expect and how to request it.keyReference/key— Key reference or inline key for the holder's proof of possession.didReference/did— DID reference or inline DID for the holder's proof of possession. Required if the issuer's supported binding methods include a DID-based method.metadata(optional) Object — Arbitrary JSON stored alongside the credential in the Credential Store.
Response Codes
201— Credential(s) received and stored successfully.
Body — List<ReceiveResponse>
[
{
"stored": "urn:uuid:224bcbc4-7436-4854-8ef1-cc6e3b8236bd",
"credential": "eyJraWQiOiJ..."
}
]
stored— The credential ID assigned in the Credential Store, ornullif no store is linked.credential— The raw issued credential string (JWT, SD-JWT, or base64-encoded mDoc).
Key Holder Reference
Both the full-flow and isolated credential-receive endpoints accept a holder key in one of three ways:
| Method | Fields | When to use |
|---|---|---|
| KMS reference | keyReference: "org.tenant.kms.key-id" | Key stored in a linked KMS Service |
| DID reference | didReference: "org.tenant.didstore.did-id" | DID stored in a linked DID Store Service |
| Inline | key: { "type": "jwk", "jwk": {...} } / did: "did:key:..." | One-off testing or external key management |
Learn more about key types (JWK, TSE, AWS KMS, HashiCorp Vault) in the KMS Service documentation.
