Issuing W3C VC Credentials via OID4VCI

This guide shows you how to issue a W3C Verifiable Credential (JWT-signed) 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.

W3C VC: The W3C Verifiable Credentials data model, here signed as a JWT (jwt_vc_json).

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

Prerequisites

Before you begin, ensure you have:

  • An Issuer2 service with a W3C credential configuration (format jwt_vc_json, with a credential_definition). See Setup.
  • 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 a W3C 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 UniversityDegree_jwt_vc_json 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-def456.

Example Request
curl -X 'POST' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v2/{target}.profile-def456/issuer-service-api/credentials/profiles' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer {yourToken}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "University Degree Profile",
  "credentialConfigurationId": "UniversityDegree_jwt_vc_json",
  "issuerKeyId": "waltid.tenant1.kms1.key1",
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "w3cVersion": "W3CV2",
  "credentialData": {
    "@context": [
      "https://www.w3.org/ns/credentials/v2",
      "https://www.w3.org/ns/credentials/examples/v2"
    ],
    "type": ["VerifiableCredential", "UniversityDegree"],
    "credentialSubject": {
      "degree": {
        "type": "BachelorDegree",
        "name": "Bachelor of Science and Arts"
      }
    }
  },
  "mapping": {
    "id": "<uuid>",
    "issuer": { "id": "<issuerDid>" },
    "credentialSubject": { "id": "<subjectDid>" },
    "validFrom": "<timestamp>",
    "validUntil": "<timestamp-in:365d>"
  }
}'

Body Parameters

  • credentialConfigurationId: String (required) - Must match a W3C (jwt_vc_json) configuration on your issuer service.
  • issuerKeyId: resourceIdentifier (required) - A key in your KMS.
  • issuerDid: String (optional) - The issuer DID.
  • credentialData: Object (required) - The credential body (@context, type, credentialSubject).
  • w3cVersion: String (optional) - W3CV11 or W3CV2. Controls which VCDM version is issued and whether issuanceDate/expirationDate or validFrom/validUntil are used.

See Create a Profile for every field.

When w3cVersion is W3CV2, the service applies the correct VCDM v2 @context and uses validFrom/validUntil in place of issuanceDate/expirationDate, regardless of what you put in credentialData.


Example Response
{
  "profileId": "profile-def456",
  "name": "University Degree Profile",
  "version": 1,
  "credentialConfigurationId": "UniversityDegree_jwt_vc_json",
  "createdAt": 1704067200000,
  "updatedAt": 1704067200000
}

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

Response Fields

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

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-def456/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. See Protocol Flows for the alternative.

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


Example Response
{
  "offerId": "abc123-def456-ghi789",
  "profileId": "profile-def456",
  "profileVersion": 1,
  "authMethod": "PRE_AUTHORIZED",
  "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

  • credentialOffer: String - The OID4VCI credential offer URL. Turn it into a QR code or deep link for the wallet.

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


Using the Authorization Code Flow Instead

The example above used PRE_AUTHORIZED — no user login. To have the holder authenticate against an external IdP 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