Issuing W3C Verifiable Credentials via OID4VCI
This guide shows you how to issue a W3C Verifiable Credential with a JWT signature using the walt.id Issuer2 API. You'll issue a credential from a ready-made profile in seconds, then see how to define your own.
W3C VC: A digital equivalent of physical credentials such as a driver's license or university degree, defined by the W3C Verifiable Credentials Data Model.
OID4VCI: The protocol used to deliver the credential to a wallet.
Issuer2 issues credentials from profiles — reusable configurations declared ahead of time in a config file, with sensible defaults for the issuer key and credential data. If you're used to the original Issuer (v1), where the key and credential data were posted directly to an issuance endpoint every time, see Getting Started for what changed — you can still override a profile's defaults per-offer via runtimeOverrides.
Prerequisites
Before you begin, ensure you have:
- Issuer2 API running — Follow the Setup guide
- A test wallet — Use the walt.id Wallet for testing, or any OID4VCI-compatible wallet
Local Development: When running locally via Docker, the API is available at http://localhost:7002 (standalone) or http://localhost:7005 (Docker Compose). Use this as your base URL in the examples below.
Quick Start: Issue Using the Default Profile
The waltid-identity repository ships with a ready-made profile called universityDegree, bound to the UniversityDegree_jwt_vc_json credential type. You can issue from it immediately — no configuration file editing required.
Step 1: Confirm the Profile Is Loaded
Endpoint: GET /issuer2/profiles/{profileId} | API Reference
Example Request
curl -X 'GET' \
'http://localhost:7002/issuer2/profiles/universityDegree' \
-H 'accept: application/json'
Example Response
{
"profileId": "universityDegree",
"name": "UniversityDegree",
"credentialConfigurationId": "UniversityDegree_jwt_vc_json",
"issuerKey": {
"type": "jwk",
"jwk": {
"d": "KJ4k3Vcl5Sj9Mfq4rrNXBm2MoPoY3_Ak_PIR_EgsFhQ",
"crv": "P-256",
"x": "G0RINBiF-oQUD3d5DGnegQuXenI29JDaMGoMvioKRBM",
"y": "ed3eFGs2pEtrp7vAZ7BLcbrUtpKkYWAT2JPUQK4lN4E",
"kty": "EC"
}
},
"issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IkcwUklOQmlGLW9RVUQzZDVER25lZ1F1WGVuSTI5SkRhTUdvTXZpb0tSQk0iLCJ5IjoiZWQzZUZHczJwRXRycDd2QVo3QkxjYnJVdHBLa1lXQVQySlBVUUs0bE40RSJ9",
"credentialData": {
"issuanceDate": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
"issuer": {
"id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
},
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.gov/credentials/3732",
"credentialSubject": {
"degree": {
"name": "Bachelor of Science and Arts",
"type": "BachelorDegree"
},
"id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
},
"type": [
"VerifiableCredential",
"UniversityDegree"
]
},
"mapping": {
"issuanceDate": "<timestamp>",
"credentialSubject": {
"id": "<subjectDid>"
},
"id": "<uuid>",
"issuer": {
"id": "<issuerDid>"
}
}
}
This is a minimal profile — no selective disclosure, no status list — a good first look at the plain shape of a profile before you customize one.
Step 2: Create a Credential Offer
Endpoint: POST /issuer2/credential-offers | API Reference
Example Request
curl -X 'POST' \
'http://localhost:7002/issuer2/credential-offers' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED"
}'
Body Parameters
- profileId: String (required) - The profile to issue from. Here, the shipped default
universityDegree. - authMethod: String (required) - The OID4VCI flow used to claim the offer.
PRE_AUTHORIZEDrequires no user login and is the simplest way to get started. See Using the Authorization Code Flow Instead for the alternative.
Example Response
{
"offerId": "477ecf7e-e783-4326-85dd-a590e94d06e6",
"profileId": "universityDegree",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1782909743958,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fissuer-api2%3A7002%2Fopenid4vci%2Fcredential-offer%3Fid%3D477ecf7e-e783-4326-85dd-a590e94d06e6"
}
Response Fields
- offerId: String - Unique identifier for this offer.
- expiresAt: Long - Unix timestamp (ms) when the offer expires (default: 300 seconds after creation).
- credentialOffer: String - The OID4VCI credential offer URL. Turn this into a QR code or deep link for the wallet.
Claiming the Offer
- Generate a QR code from the
credentialOfferURL, or use it directly as a deep link - Open the walt.id Wallet and log in
- Click "Request Credential" and paste the offer URL into the field below the camera
- The wallet resolves the offer, requests the credential, and stores it
🎉 You've issued a W3C Verifiable Credential using OID4VCI.
To learn how to accept this credential offer programmatically instead, see How to Accept W3C VCs via OID4VCI.
Defining Your Own Profile
To issue your own W3C VC type instead of the shipped default, two config files need an entry, and the service needs a restart to pick them up.
Step 1: Declare the Credential Type
Add an entry to credential-issuer-metadata.conf:
credentialConfigurations = {
"MyCredential_jwt_vc_json" = {
format = "jwt_vc_json"
scope = "MyCredential_jwt_vc_json"
cryptographic_binding_methods_supported = ["jwk", "did:key", "did:web"]
credential_signing_alg_values_supported = ["ES256"]
proof_types_supported = {
jwt = {
proof_signing_alg_values_supported = ["ES256"]
}
}
credential_definition = {
type = ["VerifiableCredential", "MyCredential"]
}
}
}
See Credential Issuer Metadata for the full field reference.
Step 2: Define a Profile
Add a matching profile to issuer2-profiles.conf:
profiles {
myCredential {
name = "My Credential"
credentialConfigurationId = "MyCredential_jwt_vc_json"
issuerKey = ${defaultIssuerKey}
issuerDid = ${defaultIssuerDid}
credentialData = {
"@context" = ["https://www.w3.org/2018/credentials/v1"]
type = ["VerifiableCredential", "MyCredential"]
credentialSubject = {
name = "Jane Smith"
}
}
mapping = {
id = "<uuid>"
issuer = { id = "<issuerDid>" }
credentialSubject = { id = "<subjectDid>" }
issuanceDate = "<timestamp>"
expirationDate = "<timestamp-in:365d>"
}
}
}
See Issuer Profiles Configuration for every available profile field, including notifications and credential status.
Step 3: Restart and Verify
Profiles are loaded once, at startup. Restart the Issuer2 container after editing either config file, then confirm the new profile is loaded.
curl -X 'GET' 'http://localhost:7002/issuer2/profiles/myCredential'
Then repeat the two steps from Quick Start above, substituting your own profileId.
Using the Authorization Code Flow Instead
The example above used PRE_AUTHORIZED — no user login required. Issuer2 also supports the Authorization Code Flow, where the holder authenticates against an external IdP (e.g. Keycloak) before claiming the credential, and the resulting ID token claims are mapped into the credential via idTokenClaimsMapping.
This requires registering an external OAuth/OIDC provider first. See Authorization Code Flow for the full sequence and setup.
Credential Status (Revocation)
To make a W3C VC revocable, embed a Bitstring Status List reference in the profile. See Credential Status for the full reference.
Next Steps
- Verify the credential you just issued — Try Verifying W3C VCs with the walt.id Verifier2 API
- Populate credentials dynamically — See Data Functions for webhooks, timestamps, and more
- Track issuance events — See Notifications for webhook and SSE options
- Issue other credential types — Try SD-JWT VC or mDL
