DCQL: The Digital Credentials Query Language

DCQL (Digital Credentials Query Language) is the mechanism verifiers use inside an OpenID4VP request to express exactly which credentials a wallet should present, and in what combinations. It was introduced in OpenID4VP Draft 22 (October 2024) and is part of the final v1.0 specification, replacing the older Presentation Exchange / Presentation Definition approach.

This guide explains how DCQL is structured, what each part does, and where the common implementation mistakes occur.

The Mental Model

DCQL is the verifier's answer to one specific question: "out of the credentials the wallet holds, which ones — and in what combinations — can satisfy what I need from this transaction?"

There are only ever two layers of decision:

  1. A list of credential queries — "I want a credential that looks like X."
  2. (Optional) logic over those queries — "I need (A and B), or (A and C), but never just A on its own."

That is the entire shape of the language. Every detail in the specification hangs off those two ideas.

Compared to Presentation Exchange (which DCQL replaces), this is dramatically simpler. Presentation Exchange required JSON Schema-like constraint descriptors with JSONPath fragments and nested filter blocks. DCQL replaces that constraint-language sprawl with named fields, a single claim path syntax, and explicit AND/OR operators.

The Top-Level Shape

Every DCQL query is an object with two possible keys:

{
  "credentials": [ ... ],
  "credential_sets": [ ... ]
}
  • credentials is required — your list of "I want a credential like X" entries.
  • credential_sets is optional — AND/OR logic across those entries.

If you only provide credentials and no credential_sets, the wallet must satisfy every entry — an implicit AND. You add credential_sets only when you need OR, optionality, or alternative combinations.

This is where most implementations over-engineer. If the use case is "give me a PID and an employment credential," do not reach for credential_sets. Two entries in credentials already expresses that.

Anatomy of a Credential Query

Each entry in credentials follows this structure:

{
  "id": "digitalID",
  "format": "dc+sd-jwt",
  "meta": { "vct_values": ["https://issuer.demo.walt.id/draft13/digitalID"] },
  "claims": [ ... ],
  "claim_sets": [ ... ],
  "trusted_authorities": [ ... ],
  "require_cryptographic_holder_binding": true,
  "multiple": false
}

The required fields are id, format, and meta. Everything else is optional and each unlocks one specific capability.

FieldRequiredDescription
idYesYour handle for this query. The wallet keys its vp_token response by these ids. Must be unique within the request.
formatYesCredential format identifier: dc+sd-jwt, mso_mdoc, jwt_vc_json.
metaYesFormat-specific filter. The structure differs per format — see the Format-Specific Gotchas section below.
claimsNoLimits which claims the wallet should disclose. Omitting this is not the same as requesting everything — see below.
claim_setsNoAlternative combinations of claims within this single credential.
trusted_authoritiesNoRestricts matches to credentials from specific trust anchors.
require_cryptographic_holder_bindingNoDefaults to true. Set false only when a key binding proof is genuinely not needed.
multipleNoDefaults to false. Set true to allow the wallet to return more than one matching credential of the same type.

The minimum viable query is exactly the three required fields and nothing else. The wallet returns the entire credential.

Selective Disclosure: Claims and the Claims Path Pointer

Add a claims array to tell the wallet which fields you actually need:

"claims": [
  { "path": ["given_name"] },
  { "path": ["family_name"] },
  { "path": ["age_over_18"] }
]

The path syntax

path is an array, not a string. Each segment is one of:

  • A string — an object key (e.g. "address")
  • An integer — an array index
  • null — matches all elements at that array level

So ["address", "locality"] means "the locality field inside the address object." This is the Claims Path Pointer defined in §7 of the spec. It is not JSONPath, it is not JSON Pointer — it is its own syntax and you will trip on it if you assume otherwise.

Format affects path semantics

For JSON-based credentials (dc+sd-jwt, jwt_vc_json), paths walk the credential's claim structure as you'd expect.

For mdoc, the rule is explicit: the first segment is the namespace, the second is the element identifier, and that is the entire path — mdoc paths are always exactly two segments long. So for an mDL given_name, you write:

{ "path": ["org.iso.18013.5.1", "given_name"] }

What "no claims" means

If claims is absent, you are requesting no selectively-disclosable claims. For an SD-JWT VC, the wallet returns only the always-disclosed envelope and the key binding JWT. Omitting claims does not mean "give me everything."

Value Matching with values

You can constrain a claim to a specific value:

{ "path": ["nationality"], "values": ["AT", "DE"] }

The wallet should return the claim only if its type and value match at least one of the listed values. Strings, integers, and booleans are accepted.

Value matching leaks information about claim non-possession. If the wallet refuses because the nationality is not in the list, the verifier learns the holder's nationality is something else.

For mdoc credentials, CBOR values must be JSON-converted (per RFC 8949 §6.1) before matching. That conversion is not always lossless — date types, byte strings, and similar types can behave unexpectedly. Do not rely on value matching for mdoc data types beyond the simple primitives.

claim_sets — Alternative Claim Combinations Within One Credential

claim_sets answers the question: "which combinations of claims would satisfy me from this single credential?"

"claims": [
  { "id": "gn",     "path": ["given_name"] },
  { "id": "fn",     "path": ["family_name"] },
  { "id": "dob",    "path": ["birth_date"] },
  { "id": "over18", "path": ["age_over_18"] }
],
"claim_sets": [
  ["gn", "fn", "over18"],
  ["gn", "fn", "dob"]
]

Read this as: "I would accept either {given_name, family_name, age_over_18} or {given_name, family_name, birth_date}." The order of the sets expresses the verifier's preference — the wallet should return the first option it can satisfy, but is not strictly required to. Put the set you most prefer first: if you want to signal a preference for minimum disclosure, list the smaller or more privacy-preserving set earlier.

Once claim_sets is used, the id field on each entry in claims becomes mandatory — the sets reference claims by those ids.

claim_sets is for alternative claim combinations within one credential. It is not a mechanism for making individual claims optional. DCQL has no concept of an optional claim by design. If you need to express an optional claim, there are two patterns:

  1. Within a single credential query — use claim_sets with the "richer" set first and the minimal set as a fallback. The wallet will return the richer set if it can satisfy it, and fall back to the minimal one if not. This is not true optionality (the user has no choice), but it gracefully handles credentials that may or may not contain a given claim.
  2. Across two credential queries — model the optional claim as a separate credential query inside an optional credential_set (see the next section). This is the more explicit pattern and ties the optionality to a declared purpose.

Multiple Credentials — Implicit AND

Two entries in credentials with no credential_sets:

{
  "credentials": [
    { "id": "pid",        "format": "dc+sd-jwt", "meta": { ... }, "claims": [ ... ] },
    { "id": "employment", "format": "dc+sd-jwt", "meta": { ... }, "claims": [ ... ] }
  ]
}

Both must be satisfied. The vp_token in the response will be an object with two keys — pid and employment — each holding a presentation. The keys are exactly the id values you chose in the query, so pick readable names.

By default, each key maps to a single presentation. If you set multiple: true on a credential query, the wallet may return an array of presentations for that key instead.

credential_sets — Explicit Logic Across Credentials

credential_sets is where you express OR, optionality, and alternative credential combinations. Each set has two properties:

"credential_sets": [
  {
    "options": [ ["pid"], ["mdl"] ],
    "required": true
  }
]
  • options is an array of arrays. Each inner array is one acceptable combination of credential ids. The wallet must satisfy at least one inner array in full.
  • [["pid"], ["mdl"]] reads as "(pid) OR (mdl)."
  • To express "(pid AND id_card) OR (mdl)" you write [["pid", "id_card"], ["mdl"]].
  • required defaults to true. Setting it to false makes the entire set optional.

Modelling optional credentials — "give me a PID, and if you happen to have a diploma, include it":

"credential_sets": [
  { "options": [["pid"]] },
  { "required": false, "options": [["diploma"]] }
]

When you use credential_sets, the sets become the sole source of truth for what is required. The credentials array defines the universe of possible credential shapes — their format, claims, and constraints — but it is the sets that determine what actually gets requested. Entries in credentials that are not referenced by any set are inert: the wallet has no instruction to return them.

Trust Framework Filtering with trusted_authorities

For EUDI and other regulated ecosystems, trusted_authorities is where policy constraints end up:

"trusted_authorities": [
  { "type": "etsi_tl", "values": ["https://eu-lotl.example/lotl.xml"] }
]

Three authority types are defined:

TypeDescription
akiAuthority Key Identifier, base64url-encoded. The values are the key identifiers of the trusted issuer CA certificates (their Subject Key Identifier). A credential matches if its certificate chain contains a certificate whose AKI extension's keyIdentifier component references one of those trusted CAs. Use when trusting a specific issuer CA key directly.
etsi_tlAn ETSI Trusted List URL (TS 119 612). The wallet walks the LOTL/TL cascade and matches if any cert in the credential's chain appears on the referenced trust list. The natural fit for QTSP-issued credentials under eIDAS.
openid_federationAn Entity Identifier. The wallet must be able to construct a valid trust chain from the credential's issuer up to that entity.

The spec is explicit: trusted_authorities is a data-minimization hint to the wallet, not a verifier-side trust check. The verifier is still responsible for independently verifying issuer trust during validation. Do not treat this field as a substitute for that check.

Holder Binding

"require_cryptographic_holder_binding": false

This field defaults to true. Set it to false only when the credential genuinely does not require cryptographic key binding — cinema tickets, biometric-bound credentials (mDL portrait), or claims-bound diplomas.

There are real consequences when you set it to false:

  • The Verifiable Presentation will not include a key binding proof, making the credential replayable.
  • Nonce-based replay protection no longer applies. The spec requires state in the Authorization Request with at least 128 bits of entropy as a substitute.

Format-Specific Gotchas — The meta Block

The meta object has a different shape for each credential format.

SD-JWT VC (dc+sd-jwt)

"meta": { "vct_values": ["https://issuer.demo.walt.id/draft13/EducationalID"] }

vct_values is an array of strings. The wallet matches if the credential's vct claim equals exactly one of the values listed.

mdoc (mso_mdoc)

"meta": { "doctype_value": "org.iso.18013.5.1.mDL" }

doctype_value is a single string, not an array. The wallet matches against the credential's MSO doctype.

W3C VCDM (jwt_vc_json)

"meta": {
  "type_values": [
    ["VerifiableCredential", "UniversityDegreeCredential"]
  ]
}

type_values is an array of arrays of strings. The outer array is OR; the inner array is AND-over-types. So [["VerifiableCredential", "UniversityDegreeCredential"]] means "the credential's type array contains both of these values." This is easy to get wrong — a single-level array is a common mistake.

Request credentials using DCQL with walt.id

The Community Stack verifier API accepts DCQL queries directly inside an OpenID4VP presentation request. Pass your dcql_query object in the request body when initiating a verification session.

  • OpenID4VP — The presentation protocol that carries the dcql_query inside an Authorization Request.
  • DC API — Browser-native credential presentation, also using dcql_query as its query format.
  • HAIP — The high-assurance profile that also uses DCQL and which is imporant for eIDAS 2.q
Last updated on May 7, 2026