+ What are you building? + Each role sets your scope before you see any code. + + + + + Wallet + The app the resident uses to hold their license or certificate. + You implement: an exchange client — POST, handle VPRs, return VPs. + ~3 things, a few days + + + + + Issuer (agency or licensing board) + The DMV, state agency, or licensing authority issuing the credential. + You implement: start an exchange, deliver a signed VC. + small, separate piece + + + + + Verifier (business or service) + Whoever checks the credential — a bar, an employer, a government portal. + You implement: request a presentation, verify the proof. + small, separate piece + + + + + You do NOT need to implement the other two. +
+ Build an issuer + + You run the agency side — a DMV, a licensing board, or a state agency. As a + coordinator you speak VCALM — just the coordinator side of + it. You don't run VCALM delivery, the on-the-wire exchange with the + resident's wallet; a workflow service does that. Your whole job is + three calls: create an exchange, share an interaction URL that points the + resident's wallet at it, then poll the exchange until the wallet has been + issued the credential. + + + + + What the coordinator does + + Create an exchange by POSTing the workflow's variables + to its exchanges endpoint. + Host and share an interaction URL that points the + resident's wallet at that exchange. + Poll the exchange until it completes, then read the + result. + + + The workflow service does everything in between — protocol negotiation, + DID authentication, signing the credential, talking to whatever wallet shows + up — and hands you back just the result. + + + 1. Create the exchange + + POST to your workflow's exchanges endpoint. The body carries a + ttl and the variables the workflow expects — here, + who the license is for and its details: + + {% highlight "bash" %} +POST https://workflows.example/workflows/ambulance-license-issuance/exchanges + {% endhighlight %} + {% highlight "json" %} +{ + "ttl": 900, + "variables": { + "subject": "did:example:resident456", + "licenseClass": "Ambulance Driver", + "licenseNumber": "ADL-2026-00123", + "validFrom": "2026-01-15T00:00:00Z", + "validUntil": "2028-01-14T23:59:59Z" + } +} + {% endhighlight %} + + This call is authenticated (your service uses a capability or OAuth2 token). + On success the service returns 204 No Content with a + Location header — that's your exchange URL: + + {% highlight "http" %} +HTTP/1.1 204 No Content +Location: https://workflows.example/workflows/ambulance-license-issuance/exchanges/z19xQ... + {% endhighlight %} + + 2. Host and share the interaction URL + + The exchange URL is the vcapi interaction endpoint. + Hosting and sharing it is the coordinator's responsibility — put it in a QR + code on a portal confirmation page, typically wrapped in a presentation + request the wallet understands: + + {% highlight "json" %} +{ + "VerifiablePresentation": { + "interact": { + "service": [{ + "type": "VerifiableCredentialApiExchangeService", + "serviceEndpoint": "https://workflows.example/workflows/ambulance-license-issuance/exchanges/z19xQ..." + }] + } + } +} + {% endhighlight %} + + The resident's wallet POSTs to that endpoint and runs the exchange with the + workflow service directly. DID authentication and credential delivery happen + there — you are not in that loop. + + + 3. Poll for the result + + While the wallet works, GET the exchange URL (authenticated) until its + state is complete: + + {% highlight "bash" %} +GET https://workflows.example/workflows/ambulance-license-issuance/exchanges/z19xQ... + {% endhighlight %} + {% highlight "json" %} +{ + "exchange": { + "id": "z19xQ...", + "state": "complete", + "variables": { + "results": { + "didAuthn": { + "did": "did:example:resident456", + "verifiablePresentation": { "...": "the wallet's authenticated VP" } + } + } + } + } +} + {% endhighlight %} + + Verified per-step results land under + exchange.variables.results, keyed by the workflow's step names. + A complete state means the credential was issued into the + resident's wallet. + + + That's it + + A government issuer, as a coordinator, is these three calls: create + an exchange, host and share the interaction URL, poll for the result. + Signing the credential and speaking every wallet's protocol is the workflow + service's job — you just create exchanges and read what comes back. + + + + On the wallet end, a standalone client-side VCALM library is still planned; + until it ships, wallets run the exchange over the raw HTTP flow. The + coordinator side shown here works today against any VCALM workflow service, + such as one running + @bedrock/vc-delivery. + + + Go deeper + + @bedrock/vc-delivery — workflow service + VCALM: DID Authentication + W3C Verifiable Credentials Data Model 2.0 + VCALM specification + + + ← Back to roles +
+ Build a verifier + + You run the relying-party side — a bar, a dispensary, an employer, an online + service. As a coordinator you speak VCALM — just the + coordinator side of it. You don't run VCALM delivery, the on-the-wire + exchange with the resident's wallet; a workflow service does that and + verifies the proof for you. Your job is three calls: create an exchange, share + an interaction URL that points the resident's wallet at it, then poll until the + verified result comes back. + + + + + What the coordinator does + + Create an exchange against a workflow that requests only + what you need — here, proof of age or proof of a valid license. + Host and share an interaction URL that points the + resident's wallet at that exchange. + Poll the exchange until it completes, then read the + verified result. + + + The workflow service does the rest — it sends the presentation request, + receives the wallet's response, and verifies the proof, challenge, domain, + issuer, and status before it ever returns a result to you. + + + 1. Create the exchange + + POST to your workflow's exchanges endpoint. The workflow already + encodes request only "age ≥ 21" and trust the issuing + agency; you pass just the per-exchange variables: + + {% highlight "bash" %} +POST https://workflows.example/workflows/age-check/exchanges + {% endhighlight %} + {% highlight "json" %} +{ + "ttl": 300, + "variables": { + "reason": "Please prove you are 21 or older to continue." + } +} + {% endhighlight %} + + This call is authenticated. On success the service returns + 204 No Content with a Location header — your + exchange URL: + + {% highlight "http" %} +HTTP/1.1 204 No Content +Location: https://workflows.example/workflows/age-check/exchanges/z19xQ... + {% endhighlight %} + + Because the workflow asks for a single derived attribute, a well-built wallet + returns proof of "age ≥ 21" — not the resident's name, exact birthdate, or + home address. The workflow's challenge and domain + stop a captured presentation from being replayed. + + + 2. Host and share the interaction URL + + The exchange URL is the vcapi interaction endpoint. + Put it in a QR code at the point of sale, or embed it as a link in an + online flow — typically wrapped in a presentation request: + + {% highlight "json" %} +{ + "VerifiablePresentation": { + "interact": { + "service": [{ + "type": "VerifiableCredentialApiExchangeService", + "serviceEndpoint": "https://workflows.example/workflows/age-check/exchanges/z19xQ..." + }] + } + } +} + {% endhighlight %} + + The resident's wallet POSTs to that endpoint and runs the exchange with the + workflow service directly. You are not in that loop. + + + 3. Poll for the verified result + + GET the exchange URL (authenticated) until its state is + complete: + + {% highlight "bash" %} +GET https://workflows.example/workflows/age-check/exchanges/z19xQ... + {% endhighlight %} + {% highlight "json" %} +{ + "exchange": { + "id": "z19xQ...", + "state": "complete", + "variables": { + "results": { + "ageCheck": { + "did": "did:example:resident456", + "verifiablePresentation": { "...": "verified: ageAtLeast21 = true" } + } + } + } + } +} + {% endhighlight %} + + A complete state means the workflow service already verified the + presentation for you — proof, challenge, domain, trusted issuer, and + revocation/expiry status. The verified result lands under + exchange.variables.results, keyed by the workflow's step names. + You received one derived fact — not the resident's ID. + + + License check variant + + The same three-call pattern works for checking professional licenses. + Configure a workflow that requests proof of a valid license class and trusts + the relevant licensing board: + + {% highlight "json" %} +{ + "ttl": 300, + "variables": { + "reason": "Please prove you hold a current ambulance driver certificate." + } +} + {% endhighlight %} + + A complete exchange confirms the license is current, not + expired, and signed by a trusted authority — without a real-time lookup to + the licensing board's registry. + + + That's it + + A relying-party verifier, as a coordinator, is these three calls: + create an exchange, host and share the interaction URL, poll for the + verified result. The cryptographic verification and protocol interop + are the workflow service's job — you create exchanges and read what comes + back. + + + + On the wallet end, a standalone client-side VCALM library is still planned; + until it ships, wallets run the exchange over the raw HTTP flow. The + coordinator side shown here works today against any VCALM workflow service, + such as one running + @bedrock/vc-delivery. + + + Go deeper + + @bedrock/vc-delivery — workflow service + VCALM: QueryByExample + VCALM: Verifiable Presentation Request + VCALM specification + + + ← Back to roles +
+ Build a wallet + Make your wallet speak VCALM: pick up and present a government credential over an exchange. + + You'll do exactly 3 things + + Dereference an interaction URL to discover protocols. + POST to the exchange URL to start. + Loop: read verifiablePresentationRequest / + verifiablePresentation, POST back, until done. + + + 1. Discover protocols + A scanned QR code (from a DMV portal or relying-party service) encodes an interaction URL with iuv=1. GET it: + {% highlight "http" %} +GET /interactions/license-456?iuv=1 +Host: dmv.example.gov +Accept: application/json + {% endhighlight %} + The response lists supported protocols; pick vcapi for VCALM: + {% highlight "json" %} +{ + "protocols": { + "vcapi": "https://vcapi.dmv.example.gov/exchanges/license-456" + } +} + {% endhighlight %} + + 2. Start the exchange + POST an empty body to the vcapi URL: + {% highlight "http" %} +POST /exchanges/license-456 +Host: vcapi.dmv.example.gov +Content-Type: application/json + +{} + {% endhighlight %} + + 3. Run the loop + + When the server returns a verifiablePresentationRequest, find the + matching credential, build a signed Verifiable Presentation — disclosing only + the requested fields — and POST it back to the same exchange + URL. When the server returns a verifiablePresentation, + store the delivered credential. The exchange is done when the server stops + asking. + + + For a verification exchange (age check, license check), the server returns a + verifiablePresentationRequest asking for a derived attribute. Your + wallet presents only that fact — not the full credential. The exchange + completes when the verifier's workflow service confirms the proof. + + + That's it + A conformant government-credential wallet is this loop. Nothing more. + + + Prefer a library to raw HTTP? + + A dogfooded open-source VCALM exchange-client library is planned. Until + it ships, the HTTP flow above is the supported path. + + + + Go deeper + + VCALM: verifiable presentation request + VCALM specification + + + ← Back to roles +
+ What it feels like to use + + The technology disappears. Here is what each person actually + does — and what they no longer have to. + + + + + The agency issues a credential + + A resident applies through the agency's portal or in person. They + authenticate once, pick the wallet they already use, and the credential + lands in it — a digital copy of their license or certificate, signed + by the agency. + + No government-mandated app. Their wallet, their choice. + + + The resident holds and controls it + + The credential lives on their phone, under their control. Nothing is + shared automatically — every presentation needs an explicit, one-time + approval the resident sees and consents to. + + The agency cannot watch where it is used after issuance. + + + They prove just one thing, when needed + + At a bar, the resident taps to prove "age ≥ 21" — and reveals + nothing else. No home address, no exact birthdate, no license number. + At a job site, they prove "valid Class A CDL" and the employer + gets confirmation — not a full ID scan. + + Selective disclosure: minimum data, every time. + + + + + + {{ flowIcons.person | safe }}What the resident experiences + + Their choice of wallet. Not a state-approved app. + Prove a fact, not your full ID. "Over 21" without your home address. + One-tap consent. Nothing leaves the phone without approval. + No tracking. The agency doesn't see each use. + + + + {{ flowIcons.institution | safe }}What the agency experiences + + Privacy by design. Data minimization built into every request. + Instant, tamper-evident checks. No real-time DMV lookup. + No wallet certification burden. Any conformant wallet works. + Standards-based. Built on open W3C Verifiable Credentials. + + + +
+ Credential types + Any government-issued document can be expressed as a W3C Verifiable Credential: + + Driver's licenses — state-issued, class-coded, with expiry. + Professional licenses — ambulance driver, contractor, cosmetologist, nurse. Prove the license is current without showing the full credential. + Age verification — derived "age ≥ N" without revealing exact birthdate or address. + Vehicle registration — ownership proof that travels with the vehicle, not just in the glove box. + Residency certificates — prove you live in a jurisdiction for benefit eligibility or in-state tuition, without sharing your full address. + +
+ How it works + One diagram, three actors, one loop. + + + AGENCY RESIDENT BUSINESS / SERVICE + (issuer) (wallet of choice) (verifier) + | | | + | -- delivers --> | | + | license VC | | + | | <-- asks for -- | + | | "over 21" | + | | -- presents --> | + | | only "yes" | + | | verifies (ok) + + + + A government credential is signed JSON the resident holds and controls. + + Delivery (agency → resident) and + presentation (resident → verifier) are the same simple + exchange loop: POST, read what the server wants, POST back. + + + With selective disclosure, the resident proves a single derived fact — + "age ≥ 21," "license is valid and current" — without revealing name, exact + birthdate, home address, or license number. + + +
+ Common questions + + Can I prove I am over 21 without revealing my exact birthdate? + + Yes. With selective disclosure, a resident proves a single derived fact — + "age ≥ 21," "valid license" — without revealing name, exact birthdate, + home address, or license number. + + + Does a digital government ID lock residents to a government app? + + Not with VCALM — the credential works with any conformant wallet. The + ISO 18013-5 / mDL approach often requires device attestation and a + state-maintained list of certified apps. + + + Is a VCALM government credential private? + + Yes. Data minimization is built into every request, nothing is shared + without one-tap consent, and the agency cannot observe where the + credential is used. + + + Why VCALM vs. mDL / ISO 18013-5? + + The ISO 18013-5 mobile driver's license requires MDOC encoding, device + attestation, and state certification of every wallet. With VCALM, you + issue once to an open W3C standard and any conformant wallet works — + no per-app certification the agency has to own or maintain. + See the full comparison. + + +