Create a Credential Offer

This guide walks you through creating a credential offer from an existing profile. 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

The offer is created against the profile as the target — its full resource path, {organizationID}.{tenantID}.{issuerServiceID}.{profileId}.

CURL

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.

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 AUTHORIZED offers; omit it for PRE_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 -1 for 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 AUTHORIZED offers).
  • 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.


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: numeric or text (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

FieldDescription
issuerDidOverride the issuer DID.
issuerKeyIdOverride the signing key.
x5ChainOverride the X.509 certificate chain.
subjectIdSet the credential subject ID.
credentialDataOverride the credential data.
mappingOverride the data mapping.
selectiveDisclosureOverride the SD-JWT selective disclosure configuration.
idTokenClaimsMappingOverride the ID token claims mapping.
mDocNameSpacesDataMappingConfigOverride the mDoc data mapping.
authorizedTransactionDataTypesOverride mDoc OpenID4VP transaction-data types authorized in the MSO KeyAuthorizations.
credentialStatusOverride the credential status configuration.
notificationsOverride the notification settings.
w3cVersionOverride the W3C data model version.
loaAchievedOverride the Level of Assurance achieved (HIGH, SUBSTANTIAL, LOW). Defaults to NON_APPLICABLE. Appened to the Audit Log.
identityProofingRefIdProvide 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). Because offer-specific runtimeOverrides are correlated to the issuance session via issuer_state, set issuerStateMode to INCLUDE whenever an AUTHORIZED offer uses runtimeOverrides — otherwise, with no issuer_state, the authorization request cannot be matched back to this offer's overrides. Note that the create request itself is not rejected if you combine runtimeOverrides with OMIT (it returns 201); the overrides simply cannot take effect.


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:

  1. Displayed as a QR code – The user scans it with their wallet app.
  2. Sent as a deep link – The user clicks the link on a mobile device.
  3. 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

Last updated on August 18, 2026