Verifying
The verifier is a standalone role: it exists and works without any issuer, and it shares only the role-neutral VC channels and the public trust path. What makes the layered model powerful here is that the verifier derives its DCQL query from the same set-bound VC channels the issuer used. Because both sides bind to the same channels, the issued credential and the requested credential agree on attribute identity by construction; there is no independently hand-typed claim path to drift.
A single DCQL query spans multiple credentials, one credential query per VC channel. This walkthrough authors one DCQL that references both channels: the Employee SD-JWT and the Business Card mdoc.
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/oid4vp/dcql/preview | Derive a DCQL from channels without persisting (ungated) |
| POST | /api/v1/oid4vp/dcql/authored | Derive, persist, and record REQUEST-role lineage (license-gated) |
| POST | /oid4vp/backend/auth/requests | Create an OpenID4VP auth request referencing an authored query |
| GET | /oid4vp/backend/auth/requests/{correlationId} | Poll presentation status and read verified data |
The authoring and backend calls use the operator's OIDC bearer token; the verifier validates that cross-service token (issued by the tenant's AS) and resolves the tenant from its tenant_id claim. The wallet-facing endpoints (/oid4vp/request-uri/..., /oid4vp/auth/...) are anonymous.
DCQL Spans Multiple Credentials
DCQL (Digital Credentials Query Language) lets one query ask for several credentials and the specific claims to disclose from each. In the layered model, each credential query in the DCQL is derived from one VC channel: channel to credential query is one-to-one, and the DCQL collects them. Referencing two channels yields a two-credential DCQL. For the conceptual converter details, see DCQL Authoring.
Preview the Derived DCQL
preview derives and returns the query but persists nothing and records no lineage, so it is not license-gated. Use it to see the derived shape before committing.
- Request
- Response
POST /api/v1/oid4vp/dcql/preview
{
"channelRefs": [
{ "channelId": "11aa...employeeChannelId", "channelVersion": 1 },
{ "channelId": "22bb...businessCardChannelId", "channelVersion": 1 }
],
"intentToRetain": false
}
200 OK. Returns dcqlQuery with one credential query per channel (here, two).
{
"dcqlQuery": {
"credentials": [
{
"id": "EmployeeCredential",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://issuer.acme.example/employee"] },
"claims": [
{ "path": ["given_name"] },
{ "path": ["family_name"] },
{ "path": ["employee_id"] },
{ "path": ["job_title"] },
{ "path": ["employment_status"] },
{ "path": ["email"] },
{ "path": ["address", "country"] },
{ "path": ["employer"] }
]
},
{
"id": "BusinessCardMdoc",
"format": "mso_mdoc",
"meta": { "doctype_value": "org.acme.businesscard.1" },
"claims": [
{ "path": ["org.acme.businesscard.1", "given_name"] },
{ "path": ["org.acme.businesscard.1", "family_name"] },
{ "path": ["org.acme.businesscard.1", "job_title"] },
{ "path": ["org.acme.businesscard.1", "email"] },
{ "path": ["org.acme.businesscard.1", "postal_code"] }
]
}
]
},
"warnings": []
}
The SD-JWT credential query carries eight claims. The through-line from the set to the wire shape is visible: the set traversal ["residentialAddress","country"] maps to the VC channel's claimPath ["address","country"], which is the path the DCQL emits. The mdoc query is generated, not authored: OpenID4VP addresses every mdoc claim as [namespace, element], so the converter pairs each element with the channel's namespace (here org.acme.businesscard.1). You never type a namespaced path anywhere; the verifier derives it from the channel.
Author and Persist the DCQL
authored derives the same query, persists it to the versioned DCQL store under queryId, and records REQUEST-role usage-lineage rows for every consumed attribute path. It is license-gated.
- Request
- Response
POST /api/v1/oid4vp/dcql/authored
{
"queryId": "acme-employee-businesscard-1779392600",
"name": "Acme Employee + Business Card",
"channelRefs": [
{ "channelId": "11aa...employeeChannelId", "channelVersion": 1 },
{ "channelId": "22bb...businessCardChannelId", "channelVersion": 1 }
],
"intentToRetain": false
}
200 OK. Capture queryId and version.
{
"queryId": "acme-employee-businesscard-1779392600",
"version": 1,
"dcqlQuery": { "credentials": [ /* the same 2 credential queries as preview */ ] }
}
Request fields
| Field | Required | Notes |
|---|---|---|
queryId | yes (authored) | The id the persisted, versioned query is stored under. |
name | no | Human-readable name. |
channelRefs[] | yes | Array of { channelId, channelVersion } objects. One credential query is derived per channel; two here. |
intentToRetain | no | Stamped on every derived claim query when true. |
preview is the read-shaped, ungated derivation; authored is the gated write that persists a versioned query and records lineage. The two derive the identical DCQL; the difference is persistence and lineage.
Create the OpenID4VP Auth Request
Reference the authored query_id rather than inlining a query, so the presentation request and the authored, version-pinned query stay in lockstep.
- Request
- Response
POST /oid4vp/backend/auth/requests
{
"query_id": "acme-employee-businesscard-1779392600",
"client_id": "https://verifier.acme.example",
"response_uri": "http://acme.localhost:8082/oid4vp/auth/response"
}
200 OK.
{
"correlation_id": "BQp4oCijJdJ9oq7X7TWa9Q",
"request_uri": "openid4vp://?client_id=decentralized_identifier%3Adid%3Ajwk%3A...&request_uri=http%3A%2F%2Facme.localhost%3A8082%2Foid4vp%2Frequest-uri%2FBQp4oCijJdJ9oq7X7TWa9Q&state=BQp4oCijJdJ9oq7X7TWa9Q"
}
Request fields
| Field | Required | Notes |
|---|---|---|
query_id | yes | The authored DCQL query to use (version-pinned by the store). |
client_id | yes | The verifier's client identifier (its audience). |
response_uri | yes | Where the wallet posts the presentation (direct_post). |
The response carries a correlation_id to use when polling, and a request_uri that is an openid4vp:// deeplink. Its inner request_uri points to the signed request object (a JAR). The wallet dereferences it, verifies the signature, and reads the nonce, client_id (audience), and response_uri that bind the presentation.
The Wallet Presents
The wallet selects the disclosures the DCQL asks for, builds a key-binding JWT with the holder key (proving possession of the credential's cnf.jwk), and submits via direct_post to the verifier's response_uri. The KB-JWT's audience is the verifier's client_id, its nonce is the verifier's nonce, and its key matches the credential's holder binding exactly. This is standard OpenID4VP; see the OpenID4VP guide.
Poll Status and Read Verified Data
- Request
- Response
GET /oid4vp/backend/auth/requests/BQp4oCijJdJ9oq7X7TWa9Q
The status lifecycle runs created then request_retrieved then authorization_response_verified. On success the response carries verified_data with the verified claims:
{
"correlation_id": "BQp4oCijJdJ9oq7X7TWa9Q",
"status": "authorization_response_verified",
"verified_data": {
"credential_claims": [
{
"id": "EmployeeCredential",
"type": "dc+sd-jwt",
"claims": {
"given_name": "Anneke",
"family_name": "De Vries",
"email": "employee@acme.example",
"job_title": "Senior Engineer"
},
"presentation": "eyJ0eXAiOiJkYytzZC1qd3QiLCJraWQiOiJkaWQ6andr...<SD-JWT + disclosures + KB-JWT>"
},
{
"id": "BusinessCardMdoc",
"type": "mso_mdoc",
"claims": {
"org.acme.businesscard.1.given_name": "Anneke",
"org.acme.businesscard.1.postal_code": "1011 AB"
}
}
]
}
}
The SD-JWT claims surface as plain keys (given_name, family_name, email, job_title). The mdoc's disclosed data elements surface under the namespace-qualified key <namespace>.<elementIdentifier> (here org.acme.businesscard.1.<element>), mirroring the two-segment mdoc DCQL claims[].path array.
Before recording authorization_response_verified, the verifier validates the full chain for each credential: for the SD-JWT, the issuer signature, disclosure integrity (each presented disclosure hashes to a selective-disclosure entry), holder binding (KB-JWT signed by the credential's holder key), audience (KB-JWT audience equals the verifier client_id), and nonce and replay (KB-JWT nonce equals the issued nonce); for the mdoc, the DeviceResponse CBOR decode, DeviceAuth (COSE_Sign1) verification over the reconstructed OpenID4VP SessionTranscript, and the x5chain trust path. Then the DCQL match.
The DCQL derives from the same two VC channels the issuer rendered its designs from (see Channels). The set-bound VC channel is the single artifact both roles share, so the issued credential shape and the requested credential shape agree without any manual coordination.
Next Steps
- Provenance & Operations: the REQUEST-role lineage this authored query wrote, governance flow-through, versioning in practice, and the current API limits
- DCQL Authoring: the semantic-to-DCQL converter behind these endpoints
- OpenID4VP: the verifier protocol, the versioned DCQL store, and verifier bindings