Issuing SD-JWT VC Credentials via OID4VCI

This guide shows you how to issue an IETF SD-JWT VC credential end to end with the Enterprise Issuer2 Service. You'll create a minimal credential profile, turn it into a credential offer, and claim it in a wallet.

SD-JWT VC: An IETF credential format supporting selective disclosure, letting holders reveal only specific claims rather than the whole credential.

OID4VCI: The protocol used to deliver the credential to a wallet.

Prerequisites

Before you begin, ensure you have:

  • An Issuer2 service with an SD-JWT VC credential configuration (format dc+sd-jwt, with a vct). See Setup. The vct can be an external URL or a self-hosted {vctBaseURL} placeholder — see VCT Handling.
  • A KMS key for signing, in a KMS Service under the same tenant.
  • An issuer DID created via the DID Service.
  • A wallet to receive into — an Enterprise Stack Wallet Service (see Receiving an SD-JWT VC), or any OID4VCI-compatible wallet app.

Throughout this guide:

  • orgID is your organization ID, e.g. test.enterprise-sandbox.waltid.dev.
  • target is your issuer service, {organizationID}.{tenantID}.{issuerServiceID}, e.g. waltid.tenant1.issuer1.

Flow Overview

  1. Create a profile — Bind the identity_credential_vc+sd-jwt type to your key, DID, and a data template.
  2. Create an offer — Generate an OID4VCI offer URL from the profile.
  3. Claim the offer — Hand the URL to a wallet.

Step 1: Create a Profile

CURL

Endpoint: POST /v2/{target}.{profileId}/issuer-service-api/credentials/profiles | API Reference

The new profile's own ID is the last path segment of the target (like service creation), i.e. {target}.{profileId} — here waltid.tenant1.issuer1.profile-abc123.

Example Request
curl -X 'POST' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{target}.profile-abc123/issuer-service-api/credentials/profiles' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer {yourToken}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Identity Credential Profile",
  "credentialConfigurationId": "identity_credential_vc+sd-jwt",
  "issuerKeyId": "waltid.tenant1.kms1.key1",
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialData": {
    "given_name": "John",
    "family_name": "Doe",
    "birthdate": "1990-01-15"
  },
  "selectiveDisclosure": {
    "fields": {
      "birthdate": { "sd": true }
    }
  },
  "mapping": {
    "iat": "<timestamp-seconds>",
    "exp": "<timestamp-in-seconds:365d>"
  }
}'

Body Parameters

  • credentialConfigurationId: String (required) - Must match an SD-JWT VC (dc+sd-jwt) configuration on your issuer service.
  • issuerKeyId: resourceIdentifier (required) - A key in your KMS, e.g. waltid.tenant1.kms1.key1.
  • issuerDid: String (optional) - The issuer DID.
  • credentialData: Object (required) - The claims to issue.
  • selectiveDisclosure: Object (optional) - Which claims the holder can selectively disclose. Here, birthdate is made disclosable.

See Create a Profile for every field.


Example Response
{
  "profileId": "profile-abc123",
  "name": "Identity Credential Profile",
  "version": 1,
  "credentialConfigurationId": "identity_credential_vc+sd-jwt",
  "createdAt": 1704067200000,
  "updatedAt": 1704067200000
}

The example above is shortened — the response also echoes your submitted issuerDid, issuerKeyId, credentialData, mapping, and selectiveDisclosure.

Response Fields

  • profileId: String - The profile's own ID (the last path segment you chose), e.g. profile-abc123. The offer in Step 2 targets the full path {target}.{profileId}, e.g. waltid.tenant1.issuer1.profile-abc123.

Response Codes

  • 201 — Profile created.
  • 400 — Invalid request body.
  • 401 — Invalid or missing authentication token.

Step 2: Create a Credential Offer

Create the offer against the profile as the target — the profileId from Step 1.

CURL

Endpoint: POST /v2/{profileTarget}/issuer-service-api/credentials/offers | API Reference

Example Request
curl -X 'POST' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v2/waltid.tenant1.issuer1.profile-abc123/issuer-service-api/credentials/offers' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer {yourToken}' \
  -H 'Content-Type: application/json' \
  -d '{
  "authMethod": "PRE_AUTHORIZED"
}'

Body Parameters

  • authMethod: String (required) - PRE_AUTHORIZED requires no user login and is the simplest way to start. See Protocol Flows for the alternative.

See Create an Offer for PIN, expiration, delivery mode, and runtime overrides.


Example Response
{
  "offerId": "48f1dc54-134c-4e10-b889-17f3c0a595bf",
  "profileId": "profile-abc123",
  "profileVersion": 1,
  "authMethod": "PRE_AUTHORIZED",
  "expiresAt": 1782898162344,
  "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%3D48f1dc54-134c-4e10-b889-17f3c0a595bf"
}

Response Fields

  • credentialOffer: String - The OID4VCI credential offer URL. Turn it into a QR code or deep link for the wallet.
  • expiresAt: Long - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).

Claiming the Offer

Hand the credentialOffer URL to a wallet to receive the credential.

With the Enterprise Stack Wallet Service this is a single API call — see Receiving an SD-JWT VC, which passes the offer URL to the wallet's OID4VCI receive endpoint (Wallet API v2), resolves the offer, requests the credential, and stores it.

To claim it in any other wallet: generate a QR code from the credentialOffer URL which the wallet can scan or open it as a deep link.


Selective Disclosure

The profile above marks birthdate as selectively disclosable via selectiveDisclosure.fields. Any claim you don't mark is always disclosed. To make more claims optional, add them under fields with { "sd": true }. See Create a Profile for the full selective-disclosure and decoy options.

Using the Authorization Code Flow Instead

The example above used PRE_AUTHORIZED — no user login. To have the holder authenticate against an external IdP (e.g. Keycloak) and map ID-token claims into the credential via idTokenClaimsMapping, use authMethod: "AUTHORIZED". See Authorization Code Flow.


Next Steps

Last updated on July 20, 2026