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 acredential_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
- Create a profile — Bind the
OpenBadgeCredential_jwt_vc_jsontype to your key, DID, and a data template. - Create an offer — Generate an OID4VCI offer URL from the profile.
- Claim the offer — Hand the URL to a wallet.
Step 1: Create a Profile
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) -
W3CV11orW3CV2. Controls which VCDM version is issued and whetherissuanceDate/expirationDateorvalidFrom/validUntilare 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.
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_AUTHORIZEDrequires 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
- Verify the credential you just issued — Try Verifying W3C Credentials.
- Populate credentials dynamically — See Data Functions.
- Track issuance events — See Notifications & Session Events.
- Issue other credential types — Try SD-JWT VC or mDL.
