What you'll learn
- Parse and process credential offer URLs in OID4VCI format
- Accept credential offers via the Wallet API endpoint
- Display credential details to users before acceptance
- Handle credential offers from QR codes, links, or manual input
Learn how to accept W3C verifiable credentials (JWT/SD-JWT) via OID4VCI in a wallet using walt.id's Wallet API, including parsing credential offer URLs and displaying credentials to users.
What you'll learn
Relevant concepts
There are several methods by which a user can receive a credential into their wallet, and all are supported by the walt.id wallet API:
Irrespective of the chosen method, these are just different ways of receiving, parsing and fulfilling a request 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>
Within the URL, a query parameter credential_offer provides the actual offer as a JSON object.
Example of a Credential Offer Object
{
"credential_issuer": "https://issuer.demo.walt.id",
// This is the URL of the issuer of the credential(s)
"credentials": [
// This array contains details about the types of credentials being offered
{
"format": "jwt_vc_json",
// Specifies the format of the credential
"types": [
// An array indicating the credential types
"VerifiableCredential",
"BankId"
],
"credential_definition": {
"@context": [
// Provides the contexts of the credential
"https://www.w3.org/2018/credentials/v1"
],
"types": [
// Reiterates the type of credential being offered.
"VerifiableCredential",
"BankId"
]
}
}
],
"grants": {
// Specifies how the credentials can be obtained
"authorization_code": {
"issuer_state": "<issuer_state>"
// Contains an issuer_state, a unique identifier for the grant
},
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
// Provides details for a pre-authorized code grant type,
//including the actual pre-authorized_code and a flag user_pin_required indicating whether a PIN is required
// for user authentication.
"pre-authorized_code": "<pre-authorized_code>",
"user_pin_required": false
}
}
}
To accept a credential via the credential offer URL via the wallet API, please follow the steps below.
Endpoint: /wallet-api/wallet/{walletId}/exchange/useOfferRequest | API Reference
Example Request
curl -X 'POST' \
'http://0.0.0.0:7001/wallet-api/wallet/{walletId}/exchange/useOfferRequest?did={did}' \
-H 'accept: application/json' \
-H 'Content-Type: text/plain' \
-H 'authorization: Bearer {token}' \
-d 'openid-credential-offer://issuer.portal.walt.id/?credential_offer=<credential_offer>'
Path Parameters
walletId: String - The ID of the wallet to receive the credential into. See Accounts & Wallets for how to retrieve it.Query Parameters
did (optional): String - The DID of the wallet holder to use when accepting the credential. If omitted, the wallet's default DID is used.Header Parameters
authorization: String - Bearer token obtained from the login endpoint. Format: Bearer {token}.Body
The credential offer URL received from the issuer, provided as plain text.
openid-credential-offer://issuer.portal.walt.id/?credential_offer=<credential_offer>
When using the terminal, the credential offer URL returned from the issuer may have a % at the end. Make sure to remove this before passing it to the wallet.
Example Response
On success, the API returns a list of the credentials that were received and stored in the wallet.
[
{
"wallet": "6006b6f4-e651-46db-b6ae-e7bd6e9c40f2",
"id": "urn:uuid:c2f7e988-a4cb-48c2-bbf6-ed97f6010abe",
"document": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAjaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaWFVcE5VelZpYTFwV1NXeHVZMlp4WDB4bVgxTjFlRW95U25SUk5VaDJZWG8zZEZkUWJrRnFWVlZrY3lJc0luZ2lPaUpHV21SMmQwTTRZVWRvVW5keGVsZHdkR1ZxTUU1YVozUjNXVUZKTVZONVJtY3hiVXRFUlZSUFpuRkZJbjAiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pYWxvMFdsRkhhbXBuTFhwWVJtWkdORnBtYmxkZlpEaFhZa2xqWjBWWVNXOXVOR2hKYlhoaFl6UmlaeUlzSW5naU9pSnVNVU5vUVROSlNVMVNka1ZOU201MVYwNTVkalUwYlRWclpVUmhheTFsZUhOME4zRjZlWGRQVG1aRklpd2llU0k2SWw5cVdWTjZVMTlEYjNSUVgyZE1NR3hpVlVOeVpXVk9UbVU0TVhJd2VIRXpNbGxsU2pKME5HMXhibGtpZlEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoidXJuOnV1aWQ6YzJmN2U5ODgtYTRjYi00OGMyLWJiZjYtZWQ5N2Y2MDEwYWJlIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWUiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lhVXBOVXpWaWExcFdTV3h1WTJaeFgweG1YMU4xZUVveVNuUlJOVWgyWVhvM2RGZFFia0ZxVlZWa2N5SXNJbmdpT2lKR1dtUjJkME00WVVkb1VuZHhlbGR3ZEdWcU1FNWFaM1IzV1VGSk1WTjVSbWN4YlV0RVJWUlBabkZGSW4wIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjYtMDQtMTZUMTU6MDg6MDEuNTczMzg0MTA1WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW1OeWRpSTZJbEF0TWpVMklpd2lhMmxrSWpvaWFsbzBXbEZIYW1wbkxYcFlSbVpHTkZwbWJsZGZaRGhYWWtsalowVllTVzl1TkdoSmJYaGhZelJpWnlJc0luZ2lPaUp1TVVOb1FUTkpTVTFTZGtWTlNtNTFWMDU1ZGpVMGJUVnJaVVJoYXkxbGVITjBOM0Y2ZVhkUFRtWkZJaXdpZVNJNklsOXFXVk42VTE5RGIzUlFYMmRNTUd4aVZVTnlaV1ZPVG1VNE1YSXdlSEV6TWxsbFNqSjBORzF4YmxraWZRIiwiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIGFuZCBBcnRzIn19LCJleHBpcmF0aW9uRGF0ZSI6IjIwMjctMDQtMTZUMTU6MDg6MDEuNTczNDA3ODA1WiJ9LCJqdGkiOiJ1cm46dXVpZDpjMmY3ZTk4OC1hNGNiLTQ4YzItYmJmNi1lZDk3ZjYwMTBhYmUiLCJleHAiOjE4MDc4ODgwODEsImlhdCI6MTc3NjM1MjA4MSwibmJmIjoxNzc2MzUyMDgxfQ.0pWxCrYF4LMMtzzFOnBx4kSDFTd7AZSYXfRnAA8IgJ1OSLnFXRjWsXnsL78VLOjxkEGmO8Paei3ewq2vDNn9CQ",
"disclosures": "",
"addedOn": "2026-04-16T15:08:01.604768543Z",
"pending": false,
"format": "jwt_vc_json",
"parsedDocument": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "urn:uuid:c2f7e988-a4cb-48c2-bbf6-ed97f6010abe",
"type": [
"VerifiableCredential",
"UniversityDegree"
],
"issuer": {
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiaUpNUzVia1pWSWxuY2ZxX0xmX1N1eEoySnRRNUh2YXo3dFdQbkFqVVVkcyIsIngiOiJGWmR2d0M4YUdoUndxeldwdGVqME5aZ3R3WUFJMVN5RmcxbUtERVRPZnFFIn0"
},
"issuanceDate": "2026-04-16T15:08:01.573384105Z",
"credentialSubject": {
"id": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoialo0WlFHampnLXpYRmZGNFpmbldfZDhXYkljZ0VYSW9uNGhJbXhhYzRiZyIsIngiOiJuMUNoQTNJSU1SdkVNSm51V055djU0bTVrZURhay1leHN0N3F6eXdPTmZFIiwieSI6Il9qWVN6U19Db3RQX2dMMGxiVUNyZWVOTmU4MXIweHEzMlllSjJ0NG1xblkifQ",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
},
"expirationDate": "2027-04-16T15:08:01.573407805Z"
}
}
]
Before calling the API, you may want to show the user which credentials they are about to accept. In JavaScript,
assuming you have the offer URL and a decodeOfferURL(offerURL) function that returns the parsed offer object:
var offerURL = "openid-credential-offer://...";
// Extract the offer object from the URL
var offerObject = decodeOfferURL(offerURL);
// Map credentials to a display-friendly format
var credentialDetails = offerObject["credentials"].map((credential) => {
var lastType = credential.types[credential.types.length - 1];
return {
format: credential.format,
type: lastType,
};
});
// Display credential details to the user
credentialDetails.forEach((credential) => {
console.log(`Format: ${credential.format}`);
console.log(`Type: ${credential.type}`);
});
The user would then see a list of credentials that will be received and can confirm whether they want to proceed with accepting the offer.
The above example assumes decodeOfferURL(offerURL) is implemented. In a real application, use a URL parsing
library, handle JSON decoding, and add appropriate error handling.
On this page