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.

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. 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 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.


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: 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.
msoDataField-merge MSO ValidityInfo (validFrom, validUntil, expectedUpdate) onto the profile. mDoc only.
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). 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:

  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 September 30, 2026