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 Wallet2 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 OpenBadgeCredential_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": "Open Badge Credential Profile",
  "credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
  "issuerKeyId": "waltid.tenant1.kms1.key1",
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "w3cVersion": "W3CV2",
  "credentialData": {
    "@context": [
      "https://www.w3.org/ns/credentials/v2",
      "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
      "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json"
    ],
    "type": ["VerifiableCredential", "OpenBadgeCredential"],
    "issuer": {
      "type": ["Profile"],
      "name": "Jobs for the Future (JFF)",
      "url": "https://www.jff.org/",
      "image": "https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png"
    },
    "credentialSubject": {
      "type": ["AchievementSubject"],
      "achievement": {
        "id": "https://example.com/achievements/21st-century-skills/teamwork",
        "type": ["Achievement"],
        "criteria": {
          "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
        },
        "description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
        "name": "Teamwork"
      }
    },
    "credentialSchema": [
      {
        "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json",
        "type": "1EdTechJsonSchemaValidator2019"
      }
    ]
  },
  "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": "Open Badge Credential Profile",
  "version": 1,
  "credentialConfigurationId": "OpenBadgeCredential_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 Wallet2 Service this is a single API call — see Receiving a W3C VC, which passes the offer URL to the wallet's OID4VCI receive endpoint (Wallet2), 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 August 24, 2026