Skip to main content

DCQL explained: how a verifier asks a wallet for exactly what it needs

The Digital Credentials Query Language, DCQL, is the JSON query format that OpenID4VP uses inside a presentation request. It lets a relying party describe which credentials and which claims within them it wants to see, in a way any compliant wallet can parse without a bespoke integration.

The problem DCQL solves

A business wallet can hold credentials in several formats: an SD-JWT VC company registration, an mdoc-encoded professional qualification, a W3C verifiable credential from an earlier pilot. A verifier that only needs to confirm a registration number and a legal name has no portable way to ask for exactly that, across formats, without either accepting the whole credential or hand-coding a separate request per format and per wallet vendor.

DCQL fixes the request side of that gap. It is a single JSON object, embedded in the OpenID4VP authorization request, that names one or more credential queries, each pinned to a format and a set of claim paths. A wallet evaluates the query against its stored credentials, works out which ones match, and only then asks the holder to approve releasing those specific claims. The verifier gets a predictable structure to parse regardless of which wallet the holder used.

1. Verifier

Sends an OpenID4VP request with a dcql_query

2. Wallet

Matches the query against stored credentials

3. Holder

Approves releasing only the requested claims

4. Verifier

Receives one presentation per credential query id

The shape of a DCQL query

A DCQL query is one JSON object with a credentials array and, optionally, a credential_sets array. Each entry in credentials is a credential query. Fields marked M are mandatory on that query.

dcql_query

credentials[ ]

One credential query per credential you need

id + format

Which credential, in which format

meta

Type filter, such as vct_values

claims[ ]

Claim paths to disclose

claim_sets[ ]

Acceptable claim combinations

credential_sets[ ]

Optional: which combinations of credential queries satisfy the request

FieldTypeMandatory
idstringM
formatenum: dc+sd-jwt | mso_mdoc | jwt_vc_json | ldp_vcM
metaobject, shape depends on format
claimsarray of claim queries
claim_setsarray of arrays of claim ids
trusted_authoritiesarray of objects with a type and values

Each entry in claims is itself an object: an id used to refer to it from claim_sets, and a path, an array locating the claim within the credential (for example ["legal_name"] for a top-level SD-JWT claim, or ["org", "registration_number"] for a nested one), and optionally values, a list of values the claim must match.

Worked example: a company registration check

Company registration

dc+sd-jwt

The verifier says: this is what I want to receive

  • ✓ Registration numberreg_nopath: ["registration_number"]
  • ✓ Legal namelegal_namepath: ["legal_name"]
  • ✓ Country of registrationreg_countrypath: ["registration_country"]
{
  "credentials": [
    {
      "id": "company_registration",
      "format": "dc+sd-jwt",
      "meta": {
        "vct_values": ["urn:eudi:business:company-registration:1"]
      },
      "claims": [
        { "id": "reg_no", "path": ["registration_number"] },
        { "id": "legal_name", "path": ["legal_name"] },
        { "id": "reg_country", "path": ["registration_country"] }
      ]
    }
  ]
}

Example response from the wallet

The wallet answers with a vp_token object keyed by the credential query id. Each value is an array of presentations. For dc+sd-jwt a presentation is the issuer-signed JWT, one disclosure per released claim, and a key binding JWT that ties it to the nonce and client of this request.

What the wallet sends

{
  "vp_token": {
    "company_registration": [
      "<issuer-signed JWT>~<disclosure: registration_number>~<disclosure: legal_name>~<disclosure: registration_country>~<key binding JWT>"
    ]
  }
}

Claims the verifier sees after validation

{
  "vct": "urn:eudi:business:company-registration:1",
  "registration_number": "12345678",
  "legal_name": "Example Logistics B.V.",
  "registration_country": "NL"
}

Asking for a fallback: claim_sets

claim_sets lists groups of claim ids, in order of preference. The wallet returns the first group it can fully satisfy from what the holder actually holds, instead of the verifier having to send two separate requests for a precise case and a fallback case.

Worked example: registration number, or a fallback to legal name only

1. Preferred

reg_nolegal_name

Returned when the credential carries both claims

2. Fallback

legal_name

Returned only when the first set cannot be satisfied

{
  "credentials": [
    {
      "id": "company_registration",
      "format": "dc+sd-jwt",
      "meta": { "vct_values": ["urn:eudi:business:company-registration:1"] },
      "claims": [
        { "id": "reg_no", "path": ["registration_number"] },
        { "id": "legal_name", "path": ["legal_name"] }
      ],
      "claim_sets": [
        ["reg_no", "legal_name"],
        ["legal_name"]
      ]
    }
  ]
}

Here the verifier prefers a registration number plus the legal name, but will accept the legal name on its own if the holder’s credential does not carry a registration number claim.

Example response: the fallback was used

What the wallet sends

{
  "vp_token": {
    "company_registration": [
      "<issuer-signed JWT>~<disclosure: legal_name>~<key binding JWT>"
    ]
  }
}

Claims the verifier sees after validation

{
  "vct": "urn:eudi:business:company-registration:1",
  "legal_name": "Example Logistics B.V."
}

The holder’s credential has no registration number, so the wallet satisfied the second claim set and released a single disclosure. The response does not say which set was used: the verifier reads that from the claims it receives.

Combining credentials: credential_sets

credential_sets works one level up from claim_sets. Each entry lists options, where every option is a group of credential query ids. The wallet has to satisfy one option of every required entry, which gives a verifier AND and OR logic across credentials in a single request.

Required

Company registration

AND

One of

VAT registration

OR

One of

Bank account attestation

"credential_sets": [
  { "options": [["company_registration"]] },
  { "options": [["vat_registration"], ["bank_account"]] }
]

Example response: registration plus bank account

What the wallet sends

{
  "vp_token": {
    "company_registration": [
      "<issuer-signed JWT>~<disclosures>~<key binding JWT>"
    ],
    "bank_account": [
      "<issuer-signed JWT>~<disclosures>~<key binding JWT>"
    ]
  }
}

Claims the verifier sees after validation

{
  "company_registration": {
    "vct": "urn:eudi:business:company-registration:1",
    "registration_number": "12345678",
    "legal_name": "Example Logistics B.V."
  },
  "bank_account": {
    "vct": "urn:eudi:business:bank-account:1",
    "iban": "NL91ABNA0417164300",
    "account_holder": "Example Logistics B.V."
  }
}

The holder has no VAT registration credential, so the wallet chose the second option of the second set. Query ids that were not used, here vat_registration, are simply absent from vp_token.

Only trusted issuers: trusted_authorities

trusted_authorities narrows a credential query to credentials whose issuer is backed by an authority the verifier trusts. Each entry has a type and a list of values: aki for an authority key identifier, etsi_tl for an ETSI trusted list, or openid_federation for a federation trust anchor. The wallet only offers credentials that match.

Worked example: a registration from a listed issuer

The verifier says: only from issuers on this trusted list

type: etsi_tl

https://ec.europa.eu/tools/lotl/eu-lotl.xml

Registration credential from a listed issuer

issuer is on the trusted list

✓ Matches the query

Registration credential from an unlisted issuer

issuer is not on the trusted list

✗ Does not match, not offered to the holder

{
  "credentials": [
    {
      "id": "company_registration",
      "format": "dc+sd-jwt",
      "meta": { "vct_values": ["urn:eudi:business:company-registration:1"] },
      "trusted_authorities": [
        {
          "type": "etsi_tl",
          "values": ["https://ec.europa.eu/tools/lotl/eu-lotl.xml"]
        }
      ],
      "claims": [
        { "id": "reg_no", "path": ["registration_number"] },
        { "id": "legal_name", "path": ["legal_name"] }
      ]
    }
  ]
}

Example response: only the listed issuer’s credential

What the wallet sends

{
  "vp_token": {
    "company_registration": [
      "<issuer-signed JWT>~<disclosure: registration_number>~<disclosure: legal_name>~<key binding JWT>"
    ]
  }
}

Claims the verifier sees after validation

{
  "vct": "urn:eudi:business:company-registration:1",
  "registration_number": "12345678",
  "legal_name": "Example Logistics B.V."
}

The holder also had a registration credential from an unlisted issuer, but the wallet did not offer it. trusted_authorities is a filter for the wallet, not a guarantee: the verifier still checks the issuer against the trusted list itself when it validates the presentation.

Matching a value: claims.values

A claim query can carry values, a list of strings, integers or booleans. The wallet only returns the claim when its type and value exactly match one of them, so a verifier can check a condition without asking for anything else first.

Worked example: only companies registered in the Netherlands or Belgium

The verifier says: only a company registered in one of these countries

reg_countryvalues:"NL""BE"

Dutch company

registration_country: "NL"

✓ Matches the query

German company

registration_country: "DE"

✗ Does not match, not offered to the holder

{
  "credentials": [
    {
      "id": "company_registration",
      "format": "dc+sd-jwt",
      "meta": { "vct_values": ["urn:eudi:business:company-registration:1"] },
      "claims": [
        { "id": "legal_name", "path": ["legal_name"] },
        {
          "id": "reg_country",
          "path": ["registration_country"],
          "values": ["NL", "BE"]
        }
      ]
    }
  ]
}

Example response: a Dutch company

What the wallet sends

{
  "vp_token": {
    "company_registration": [
      "<issuer-signed JWT>~<disclosure: legal_name>~<disclosure: registration_country>~<key binding JWT>"
    ]
  }
}

Claims the verifier sees after validation

{
  "vct": "urn:eudi:business:company-registration:1",
  "legal_name": "Example Logistics B.V.",
  "registration_country": "NL"
}

A German company’s credential has registration_country "DE", so it does not satisfy the query and the wallet has nothing to return for it. The verifier should still check the value in the validated claims rather than rely on the wallet’s filtering.

Format-specific constraints in meta

SD-JWT VC: vct_values

For dc+sd-jwt, meta.vct_values lists the credential type identifiers the verifier will accept. A query only matches a stored credential whose vct is one of the listed values, so a verifier that only trusts one issuer’s registration credential type lists exactly that identifier.

mso_mdoc: doctype_value and namespace

For mso_mdoc, meta.doctype_value pins the ISO 18013-5 DocType, and each claim path starts with the mdoc namespace the claim lives under rather than a plain field name, since mdoc groups claims by namespace instead of a flat object.

Where DCQL stands today

  • DCQL is defined within the OpenID4VP specification itself, not as a separate document, and has been part of the draft since the mechanism was introduced to replace an earlier dependency on DIF Presentation Exchange for OpenID4VP requests.
  • The EUDI Wallet Architecture and Reference Framework specifies OpenID4VP as the presentation protocol and, with it, DCQL as the query mechanism relying parties and wallets in the ecosystem are expected to support.
  • Wallet and verifier reference implementations in the EUDI Wallet Reference Implementation program have converged on DCQL, so new business-wallet integrations built against OpenID4VP today should assume DCQL rather than Presentation Exchange as the query format for presentation requests.

Related terms

Frequently asked questions

How is DCQL different from DIF Presentation Exchange?

Both describe what a verifier wants from a wallet, but DCQL is scoped to OpenID4VP and defined directly in that specification, while Presentation Exchange is a separate DIF specification that also covers other protocols. DCQL is deliberately smaller: it has no input descriptor groups or submission requirements, and it expresses format-specific constraints such as an mdoc doctype or an SD-JWT VC type directly in a query object rather than through a generic JSON Schema filter. The EUDI Wallet ecosystem has standardised on DCQL for OpenID4VP presentations.

Can one DCQL query ask for more than one credential?

Yes. The credentials array can list several credential queries, each with its own id. A wallet that holds matches for every entry returns one presentation per entry. The optional credential_sets object on top of that can require particular combinations, for example accept either a company registration credential alone or a company registration credential together with a UBO declaration, without asking the holder twice.

What problem do claim_sets solve inside a single credential query?

A credential does not always carry every claim a verifier would like. claim_sets lists alternative groups of claim ids that would each satisfy the request on their own, ordered from most to least preferred. The wallet picks the first group it can fully satisfy from the claims the holder actually has, so a verifier can ask for a precise ID number where available and fall back to a coarser check, such as an over-18 flag, without sending two separate requests.

Is DCQL specific to the EUDI Wallet?

No. DCQL is part of the OpenID4VP core specification and any OpenID4VP implementation can use it. The EUDI Wallet ecosystem is a prominent adopter: the Architecture and Reference Framework specifies OpenID4VP with DCQL as the presentation mechanism relying parties must support, which is why it matters in particular for wallets and verifiers built for the European market.

Does DCQL itself perform selective disclosure?

No. DCQL only describes what is being asked for. Whether the wallet can reveal exactly those claims and nothing else depends on the credential format: an SD-JWT VC or an ISO mdoc both support disclosing a subset of their claims, so a DCQL claims array maps onto that support. DCQL against a format without selective disclosure would still work, but the holder would have to release the full credential to satisfy even a one-claim query.

Sources

  1. OpenID for Verifiable Presentations 1.0, Digital Credentials Query Language (DCQL) section
  2. EUDI Wallet Architecture and Reference Framework
  3. DIF Presentation Exchange 2.0.0 specification

This page is informational and does not constitute legal advice. For authoritative guidance consult the OpenID Foundation and the European Commission directly.

Talk to us about EUDI Wallet integration