Tutorial v2 (30 min)
In this 30 min tutorial, we will take a look at the Issuer2, Wallet2, and Verifier2 APIs of the walt.id Community Stack — the latest generation of the issuer, wallet, and verifier services, implementing OID4VCI 1.0 and OID4VP 1.0.
Still building against the original Issuer, Wallet, and Verifier? Use the v1 tutorial instead.
The tutorial is split into three sections:
- Create a wallet
- Issue a credential into the wallet
- Request & verify the credential from the wallet
With that, we are covering a complete digital credential use case end-to-end using the walt.id v2 API components:
The Credential
For this tutorial, we will issue ourselves an Open Badge Credential — a
W3C Verifiable Credential with a JWT signature
(jwt_vc_json), issued from a profile that ships out of the box with Issuer2, so there's no configuration to write
before your first credential.
If you are curious to learn more about other credential formats, go here. Issuer2, Wallet2, and Verifier2 also support SD-JWT VC and mdoc (ISO 18013-5) — this tutorial sticks to W3C VC to keep the walkthrough focused.
Example Open Badge Credential (as issued)
{
"@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"
],
"id": "urn:uuid:6bc9b310-4ef2-4d0a-97e7-c577b4c944d4",
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"issuer": {
"id": "did:jwk:...",
"name": "Jobs for the Future (JFF)",
"type": ["Profile"],
"url": "https://www.jff.org/"
},
"credentialSchema": [
{
"type": "1EdTechJsonSchemaValidator2019",
"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json"
}
],
"credentialSubject": {
"id": "did:key:...",
"type": ["AchievementSubject"],
"achievement": {
"id": "https://example.com/achievements/21st-century-skills/teamwork",
"type": ["Achievement"],
"name": "Teamwork",
"description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
"criteria": {
"narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
}
}
},
"validFrom": "2026-07-22T12:50:10.122342336Z"
}
Setup
To bring up Issuer2, Wallet2, and Verifier2 locally, we'll use our docker compose setup.
Clone walt.id identity
git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity/docker-compose
Launch the services
docker compose up
Access APIs
- Issuer2 API — http://localhost:7005/
- Verifier2 API — http://localhost:7004/
- Wallet2 API — http://localhost:7006/
Create a Wallet
By default, Wallet2's auth feature is off — there's no account, no registration, no login, and no bearer token to
manage. A wallet is just a container for keys, a DIDs, and credentials, and you get its ID back the moment you create
it. Authentication can be enabled — see Authentication.
A new wallet doesn't have any key or DID yet. The key is what the wallet signs with; the DIDs are identifier derived from that key, which credentials get bound to when receiving them (new to DIDs? see Decentralised Identifiers). We generate both next, as two explicit calls right after creating the wallet.
Create the Wallet
Endpoint: POST /wallet | API Reference
curl -X POST http://localhost:7006/wallet \
-H 'Content-Type: application/json' \
-d '{}'
Body Parameters
An empty body is enough — all fields are optional for the default, fully-managed wallet. See
Managing Wallets for noDidStore and the store-less-wallet options.
Example Response
{
"walletId": "39c44fcf-63bf-41fb-a7de-d99d09458940"
}
Save the walletId — every subsequent call is scoped to it.
Generate a Key
Endpoint: POST /wallet/{walletId}/keys/generate | API Reference
curl -X POST http://localhost:7006/wallet/{walletId}/keys/generate \
-H 'Content-Type: application/json' \
-d '{ "keyType": "Ed25519" }'
Body Parameters
keyType: String (optional) - One ofEd25519,secp256r1,secp256k1,RSA. Defaults toEd25519.
Example Response
{
"keyId": "onxChakLCfOVM_YyZOOJ09fO1hgPrXxKxiBIPwTeack",
"keyType": "Ed25519"
}
This is the wallet's first (and so far only) key — it automatically becomes the default key, used for signing
whenever a later call doesn't specify keyId explicitly.
Create a DID
W3C VCs bind the credential subject to a DID, not just a key, so we need one before we can receive one.
Endpoint: POST /wallet/{walletId}/dids/create | API Reference
curl -X POST http://localhost:7006/wallet/{walletId}/dids/create \
-H 'Content-Type: application/json' \
-d '{ "method": "key" }'
Body Parameters
method: String (required) - The DID method to create. Supported:key,jwk.keyId: String (optional) - Key to derive the DID from. Defaults to the wallet's default key — the one we just generated.
Example Response
{
"did": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"document": {
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"],
"id": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"verificationMethod": [
{
"id": "did:key:z6MksLjz...#z6MksLjz...",
"type": "JsonWebKey2020",
"controller": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}
],
"assertionMethod": ["did:key:z6MksLjz...#z6MksLjz..."],
"authentication": ["did:key:z6MksLjz...#z6MksLjz..."]
}
}
This is now the wallet's default DID too. Both defaults mean every call from here on can omit keyId and did
entirely.
Issue Credential
Confirm the Profile Is Loaded (optional)
Issuer2 issues from profiles — reusable credential
configurations declared ahead of time (issuer key, issuer DID, credential template, etc.). openBadgeCredential
ships pre-configured:
Endpoint: GET /issuer2/profiles/{profileId} | API Reference
curl http://localhost:7005/issuer2/profiles/openBadgeCredential
Example Response (shortened)
{
"profileId": "openBadgeCredential",
"name": "OpenBadgeCredential",
"credentialConfigurationId": "OpenBadgeCredential_jwt_vc_json",
"issuerKey": { "type": "jwk", "jwk": { "kty": "EC", "crv": "P-256", "...": "..." } },
"issuerDid": "did:jwk:...",
"credentialData": {
"@context": ["https://www.w3.org/ns/credentials/v2", "..."],
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSubject": { "achievement": { "name": "Teamwork", "...": "..." } }
},
"mapping": {
"id": "<uuid>",
"issuer": { "id": "<issuerDid>" },
"credentialSubject": { "id": "<subjectDid>" },
"validFrom": "<timestamp>"
}
}
Create a Credential Offer
Now let's actually issue a credential from that profile. This call creates a credential offer — a one-time, short-lived pointer to a not-yet-issued credential that a wallet can redeem.
Endpoint: POST /issuer2/credential-offers | API Reference
curl -X POST http://localhost:7005/issuer2/credential-offers \
-H 'Content-Type: application/json' \
-d '{
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED"
}'
Body Parameters
profileId: String (required) - The profile to issue from.authMethod: String (required) -PRE_AUTHORIZEDrequires no user login and is the simplest way to get started. Issuer2 also supportsAUTHORIZED(authorization-code flow against an external IdP) — see Authorization Code Flow.
Example Response
{
"offerId": "e124ba49-df08-45e1-8caa-810a712122e6",
"profileId": "openBadgeCredential",
"authMethod": "PRE_AUTHORIZED",
"expiresAt": 1784724878505,
"credentialOffer": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
}
credentialOffer is the OID4VCI offer URL — hand it directly to
Wallet2 in the next step. Notice it already resolves
via host.docker.internal, which every container in the compose network can reach, so it works whether the caller is
your terminal, the demo wallet, or (as we'll do next) another container.
Receive Credential in Wallet
This single call looks up the offer's details, signs a proof that the wallet owns the key the credential will be bound to, then fetches the credential and stores it.
Endpoint: POST /wallet/{walletId}/credentials/receive | API Reference
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/receive \
-H 'Content-Type: application/json' \
-d '{
"offerUrl": "openid-credential-offer://?credential_offer_uri=http%3A%2F%2Fhost.docker.internal%3A7005%2Fopenid4vci%2Fcredential-offer%3Fid%3De124ba49-df08-45e1-8caa-810a712122e6"
}'
Body Parameters
offerUrl: String - ThecredentialOffervalue from the previous step.
Example Response
{
"credentialIds": ["3d14e4b9-9478-41ed-bb52-bcf76b7dd36c"],
"deferredTransactionIds": {}
}
Confirm It Was Stored
Endpoint: GET /wallet/{walletId}/credentials | API Reference
curl http://localhost:7006/wallet/{walletId}/credentials
Example Response
[
{
"id": "3d14e4b9-9478-41ed-bb52-bcf76b7dd36c",
"format": "jwt_vc_json",
"issuer": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Ii...",
"subject": "did:key:z6MksLjzRVB47ciKofd8AmNNrtvjrEd5DfTHUsUAQtFmiimD",
"addedAt": "2026-07-22T12:50:10.138097127Z"
}
]
🎉 You've received an Open Badge Credential into the wallet. To see the full parsed contents (not just metadata), use
GET /wallet/{walletId}/credentials/{credentialId} — see
Credential Management.
Request & Verify Credential
We now switch to Verifier2 to build an OID4VP authorization request.
Verifier2 uses
DCQL (Credential Query Language) to describe the
credentials it wants — the credential type/format is expressed as a dcql_query directly in the request body.
Endpoint: POST /verification-session/create | API Reference
curl -X POST http://localhost:7004/verification-session/create \
-H 'Content-Type: application/json' \
-d '{
"flow_type": "cross_device",
"core_flow": {
"dcql_query": {
"credentials": [
{
"id": "credential_1",
"format": "jwt_vc_json",
"meta": { "type_values": [["VerifiableCredential", "OpenBadgeCredential"]] }
}
]
},
"policies": {
"vc_policies": [{ "policy": "signature" }]
}
}
}'
Body Parameters
flow_type: String (required) -cross_device(QR code),same_device(deep link),dc_api/dc_api-annex-c(browser-native Digital Credentials API). We usecross_devicehere; it needs no extra flow-specific fields.core_flow.dcql_query.credentials: Array (required) - one entry per credential to request.idis a free-choice label used later to key policy results;format+meta.type_valuesdescribe what to accept — here, any credential whosetypearray matches["VerifiableCredential", "OpenBadgeCredential"].core_flow.policies.vc_policies: Array (optional) - validation rules applied to the received credential. See Policies for the full list. Omittingpoliciesentirely defaults to signature-only verification — the same as what we set explicitly here.
Example Response
{
"sessionId": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
"bootstrapAuthorizationRequestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest",
"fullAuthorizationRequestUrl": "openid4vp://authorize?response_type=vp_token&client_id=verifier2&state=1c7706bd-8e23-41a6-8bd9-291c51e8010a&response_mode=direct_post&nonce=1ccec52e-f0d0-48fc-b51e-fc1115c5d930&response_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Fresponse&dcql_query=%7B%22credentials%22%3A%5B%7B%22id%22%3A%22credential_1%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22meta%22%3A%7B%22type_values%22%3A%5B%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%5D%7D%7D%5D%7D&client_metadata=..."
}
sessionId: use this to check the result later.bootstrapAuthorizationRequestUrl: the short,request_uri-by-reference form — this is what we hand to the wallet next (and what you'd turn into a QR code for a real cross-device flow).
Present Credential
A single call resolves the verifier's request, runs the DCQL match against the wallet's stored credentials, signs with the default key/DID, and submits the response.
Endpoint: POST /wallet/{walletId}/credentials/present | API Reference
curl -X POST http://localhost:7006/wallet/{walletId}/credentials/present \
-H 'Content-Type: application/json' \
-d '{
"requestUrl": "openid4vp://authorize?client_id=verifier2&request_uri=http%3A%2F%2Fhost.docker.internal%3A7004%2Fverification-session%2Fd20e3de1-0695-4c54-bb47-4104d85c41d7%2Frequest"
}'
Body Parameters
requestUrl: String - thebootstrapAuthorizationRequestUrl(orfullAuthorizationRequestUrl) from the previous step.
Example Response
{
"transmission_success": true,
"verifier_response": {
"status": "received",
"message": "Presentation received and is being processed."
}
}
transmission_success: true only confirms the wallet delivered the presentation — it says nothing about whether the
credential actually passed verification. Check that on the verifier side next.
Check the Verification Result
Back on Verifier2, poll (or subscribe via SSE/callback) the session to see the final status and policy results.
Endpoint: GET /verification-session/{sessionId}/info | API Reference
curl http://localhost:7004/verification-session/{sessionId}/info
Example Response (shortened)
{
"id": "d20e3de1-0695-4c54-bb47-4104d85c41d7",
"status": "SUCCESSFUL",
"attempted": true,
"reattemptable": true,
"policy_results": {
"vp_policies": {
"credential_1": {
"jwt_vc_json/audience-check": { "success": true },
"jwt_vc_json/nonce-check": { "success": true },
"jwt_vc_json/envelope_signature": { "success": true }
}
},
"vc_policies": [
{
"policy": { "policy": "signature", "id": "signature" },
"success": true,
"result": {
"verification_result": true,
"verified_data": {
"iss": "did:jwk:...",
"sub": "did:key:z6MksLjz...",
"vc": {
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"credentialSubject": {
"id": "did:key:z6MksLjz...",
"achievement": { "name": "Teamwork", "...": "..." }
}
}
}
}
}
],
"overallSuccess": true
},
"presented_credentials": {
"credential_1": [
{ "type": "vc-w3c_1_1", "credentialData": { "...": "the presented credential" } }
]
}
}
status: "SUCCESSFUL" and policy_results.overallSuccess: true confirm the credential was received and the
signature policy passed. 🎉
You've issued, received, requested, and verified an Open Badge Credential end-to-end using Issuer2, Wallet2, and Verifier2.
Next Steps
If you enjoy our tools, please leave us a star ⭐ on GitHub.
You can learn more about the different APIs in detail below:
