Create a Credential Offer
This guide walks you through creating a credential offer from one or more existing profiles. The offer generates an OID4VCI credential offer URL that any compliant wallet can claim.
Prerequisites
Before creating an offer, ensure you have:
- An Issuer2 service — A running issuer service. See Setup.
- A credential profile — An existing profile to issue from. See Create a Profile.
Create an Offer
A single-profile offer is created against the profile as the target — {organizationID}.{tenantID}.{issuerServiceID}.{profileId}. A multi-credential offer is created against the issuer service — {organizationID}.{tenantID}.{issuerServiceID} — and must send a credentials array.
Endpoint: POST /v2/{profileTarget}/issuer-service-api/credentials/offers | API Reference
Example Request
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{profileTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "PRE_AUTHORIZED",
"valueMode": "BY_REFERENCE",
"expiresInSeconds": 300
}'
Path Parameters
- orgID: String (required) - Your organization ID, e.g.
test.enterprise-sandbox.waltid.dev. - profileTarget: String (required) - The resource identifier of the credential profile,
{organizationID}.{tenantID}.{issuerServiceID}.{profileId}, e.g.waltid.tenant1.issuer1.profile-abc123. For several profiles in one offer, use the issuer service as the target instead — see Multiple Credentials.
Header Parameters
- Authorization: String (required) - Bearer token. Format:
Bearer {token}.
Body Parameters
- authMethod: String (required) - The OID4VCI flow used to claim the offer. Options:
"PRE_AUTHORIZED"— Pre-authorized code flow (no user authentication)."AUTHORIZED"— Authorization code flow (user authentication required).
- valueMode: String (optional) - How the offer is delivered (default:
BY_REFERENCE). Options:"BY_REFERENCE"— Offer URL contains a reference; the wallet fetches the full offer."BY_VALUE"— Full credential offer embedded in the URL.
- issuerStateMode: String (optional) - Whether to include issuer state. Valid only for
AUTHORIZEDoffers; omit it forPRE_AUTHORIZED. Options:"INCLUDE"— Include issuer state for session correlation with the authorization server."OMIT"— No issuer state.
- expiresInSeconds: Integer (optional) - Offer / issuance session validity duration in seconds (default: 300). Set to
-1for no expiration and no MongoDB TTL auto-ejection. Finite values delete the stored session after expiry. See Session Data Ejection. - txCode: Object (optional) - Transaction code (PIN) configuration for the pre-authorized flow. See Transaction Code (PIN).
- txCodeValue: String (optional) - A specific PIN value. If omitted, one is generated.
- runtimeOverrides: Object (optional) - Override profile values for this offer only. See Runtime Overrides.
- sessionId: String (optional) - Session ID for the issuance session. If omitted, a random UUID is generated.
Example Response
{
"offerId": "abc123-def456-ghi789",
"profileId": "profile-abc123",
"profileVersion": 1,
"authMethod": "PRE_AUTHORIZED",
"issuerStateMode": null,
"expiresAt": 1704067500000,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fwaltid.enterprise-sandbox.waltid.dev%2Fv2%2Fwaltid.tenant1.issuer1%2Fissuer-service-api%2Fopenid4vci%2Fcredential-offer%3Fid%3Dabc123-def456-ghi789"
}
Response Fields
- offerId: String - Unique identifier for this offer (also the issuance session ID).
- profileId: String - The profile used to create this offer.
- profileVersion: Integer - The version of the profile used.
- authMethod: String - The authentication method.
- issuerStateMode: String - The issuer state mode (present only for
AUTHORIZEDoffers). - expiresAt: Long - Unix timestamp (ms) when the offer expires.
- txCodeValue: String - The PIN value (only for pre-authorized flow with
txCode). - credentialOffer: String - The OID4VCI credential offer URL.
Response Codes
201— Offer created successfully.400— Invalid request body.401— Invalid or missing authentication token.
🎉 You've created a credential offer. Hand the credentialOffer URL to a wallet as a QR code or deep link to claim it.
Multiple Credentials
To offer several profiles — or the same profile with different credentialData overrides — in one URL, call the same path against the issuer service and send a non-empty credentials array. Each entry has a profileId and optional runtimeOverrides. Do not send top-level profileId or runtimeOverrides on this request. A profile-target request cannot use credentials[].
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{issuerTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"credentials": [
{ "profileId": "open-badge" },
{ "profileId": "iso-mdl" }
],
"authMethod": "PRE_AUTHORIZED",
"valueMode": "BY_REFERENCE"
}'
Path Parameters
- orgID: String (required) - Your organization ID, e.g.
test.enterprise-sandbox.waltid.dev. - issuerTarget: String (required) - The issuer service,
{organizationID}.{tenantID}.{issuerServiceID}, e.g.waltid.tenant1.issuer1.
Each item can carry its own runtimeOverrides:
{
"credentials": [
{
"profileId": "open-badge",
"runtimeOverrides": {
"credentialData": {
"credentialSubject": {
"achievement": { "name": "OpenID4VCI Implementation" }
}
}
}
},
{ "profileId": "iso-mdl" }
],
"authMethod": "PRE_AUTHORIZED"
}
The wallet redeems each offered item with a separate Credential Request. The create-offer response has the same common fields (offerId, authMethod, expiresAt, credentialOffer, …) but no profileId, profileVersion, or credentials array — even when the array had one entry.
If two items share the same credential_configuration_id (for example the same profile twice with different datasets), the wallet must request authorization_details at the token endpoint and then use credential_identifier on each Credential Request.
Each item can be issued once. A second Credential Request for the same credential_identifier is rejected with invalid_credential_request. The session stays ACTIVE until every selected item is issued, then becomes SUCCESSFUL and closes.
A homogeneous cryptographic batch — several holder proofs (proofs.jwt) for one of those items on a single Credential Request — is separate. Enable it on the issuer service with batchCredentialIssuance. See Setup. Each proof issues a copy of that item's dataset. If the item uses credentialStatus, Issuer2 allocates a distinct status-list entry per copy through the Credential Status Service.
Transaction Code (PIN)
For the pre-authorized flow, you can require a PIN for additional security.
{
"authMethod": "PRE_AUTHORIZED",
"txCode": {
"input_mode": "numeric",
"length": 6,
"description": "Please enter the PIN sent to your email"
}
}
txCode Parameters:
- input_mode: String (optional) - Input type:
numericortext(default:numeric). - length: Integer (optional) - Length of the PIN.
- description: String (optional) - Description shown to the user.
To set the PIN value yourself, provide txCodeValue:
{
"authMethod": "PRE_AUTHORIZED",
"txCode": {
"input_mode": "numeric",
"length": 6
},
"txCodeValue": "123456"
}
Runtime Overrides
You can override any profile value for a specific offer using runtimeOverrides:
{
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"issuerDid": "did:key:z6MkNewDid...",
"issuerKeyId": "waltid.tenant1.kms1.differentKey",
"subjectId": "did:key:z6MkSubjectDid...",
"w3cVersion": "W3CV2",
"credentialData": {
"@context": ["https://www.w3.org/ns/credentials/v2"],
"type": ["VerifiableCredential", "CustomCredential"],
"credentialSubject": { "customField": "customValue" }
},
"mapping": { "id": "<uuid>" },
"credentialStatus": {
"statusCredentialConfig": "waltid.tenant1.credentialstatus.config2",
"initialStatus": "0x0"
},
"notifications": {
"webhook": { "url": "https://different-webhook.com/callback" }
}
}
}
Available Override Fields
| Field | Description |
|---|---|
issuerDid | Override the issuer DID. |
issuerKeyId | Override the signing key. |
x5Chain | Override the X.509 certificate chain. |
subjectId | Set the credential subject ID. |
credentialData | Override the credential data. |
mapping | Override the data mapping. |
selectiveDisclosure | Override the SD-JWT selective disclosure configuration. |
idTokenClaimsMapping | Override the ID token claims mapping. |
mDocNameSpacesDataMappingConfig | Override the mDoc data mapping. |
authorizedTransactionDataTypes | Override mDoc OpenID4VP transaction-data types authorized in the MSO KeyAuthorizations. |
msoData | Field-merge MSO ValidityInfo (validFrom, validUntil, expectedUpdate) onto the profile. mDoc only. |
credentialStatus | Override the credential status configuration. |
notifications | Override the notification settings. |
w3cVersion | Override the W3C data model version. |
loaAchieved | Override the Level of Assurance achieved (HIGH, SUBSTANTIAL, LOW). Defaults to NON_APPLICABLE. Appened to the Audit Log. |
identityProofingRefId | Provide a reference ID for external identity proofing or other level of assurance requirements. Appened to the Audit Log. e.g. 1234567890 |
issuer_state correlates a credential offer with the later authorization request in the authorization code flow. The issuer creates it, the wallet treats it as opaque, and if present in the offer the wallet sends it back in the authorization request. AUTHORIZED offers omit issuer_state unless issuerStateMode is INCLUDE (the default is OMIT). Offer-specific runtimeOverrides — including per-item overrides on credentials[] — require that correlation, so AUTHORIZED offers that set any runtimeOverrides must use issuerStateMode INCLUDE. Combining overrides with OMIT is rejected.
Examples
Pre-Authorized Flow with PIN
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{profileTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "PRE_AUTHORIZED",
"txCode": {
"input_mode": "numeric",
"length": 4
},
"expiresInSeconds": 600
}'
Authorization Code Flow
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{profileTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "AUTHORIZED",
"issuerStateMode": "INCLUDE",
"expiresInSeconds": 900
}'
The same issuer_state from the offer is forwarded to the authorization server in the authorization request, e.g.:
https://keycloak.demo.walt.id/realms/myrealm/protocol/openid-connect/auth?client_id=issuer_api&redirect_uri=.../issuer-service-api/openid4vci/callback&scope=openid_profile&state=74b5206d7a15c9ec&response_type=code&issuer_state=cadbeaf4-d412-4ce3-b8d4-e8d5c70d4a3f
See Authorization Code Flow for the full sequence.
Offer with Custom Credential Data
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{profileTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "PRE_AUTHORIZED",
"runtimeOverrides": {
"credentialData": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegree"],
"credentialSubject": {
"degree": { "type": "MasterDegree", "name": "Master of Computer Science" }
}
}
}
}'
Offer by Value
curl -X 'POST' \
'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{profileTarget}/issuer-service-api/credentials/offers' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {yourToken}' \
-H 'Content-Type: application/json' \
-d '{
"authMethod": "PRE_AUTHORIZED",
"valueMode": "BY_VALUE"
}'
Using the Credential Offer
The credentialOffer URL in the response can be:
- Displayed as a QR code – The user scans it with their wallet app.
- Sent as a deep link – The user clicks the link on a mobile device.
- Embedded in an email/message – The user clicks to open in their wallet.
When the wallet resolves this URL, it fetches the full credential offer, displays the credential details to the user, and requests the credential using the appropriate flow.
Next Steps
- Notifications & Session Events – Monitor issuance progress in real time.
- Protocol Flows – Understand the pre-authorized and authorization code sequences.
- Setup – Enable Credential Endpoint
proofs.jwtbatch issuance.
