Skip to content

Creating Verification Queries

A verification query is a named, reusable statement of what you want a holder to present. You create one against the credential verification service, and every presentation you request names it by id.

Two names, one nesting

A verification query is ours: the object you create and address by verification_query_id. Inside it sits a DCQL query — Digital Credentials Query Language, the part of OID4VP that expresses which credentials and claims you are asking for. So the thing you create is a verification query; the thing you write inside it is a DCQL query.

Hold as many verification queries as you have distinct things to ask for. Each is created once and reused across sessions.

What is a DCQL query?

A DCQL query is a structured representation of the data the wallet should present to the verifier during the presentation protocol. The language is defined in OID4VP section 6, Digital Credentials Query Language. In the example below it instructs the wallet to include studentId, dateOfBirth and enrollmentYear in a verifiable presentation:

DCQL query example:
{
  "credentials": [ // (1)!
    {
      "id": "studentCard", // (2)!
      "format": "jwt_vc_json-ld", // (3)!
      "meta": {}, // (4)!
      "claims": [ // (5)!
        {
          "id": "studentId", // (6)!
          "path": [ "credentialSubject", "studentId" ] // (7)!
        },
        {
          "id": "dateOfBirth",
          "path": [ "credentialSubject", "dateOfBirth" ]
        },
        {
          "id": "enrollmentYear",
          "path": [ "credentialSubject", "enrollmentYear" ]
        }
      ]
    }
  ]
}
  1. This is an array of credential queries. Each credential query specifies the type of information (claims) the wallet must present for this requested credential. A single DCQL query can contain multiple credential queries, and the wallet must present all of them in the presentation.
  2. A string that serves as an identifier for the credential query. This value is used when the wallet prepares the presentation, and therefore must not conflict with other IDs.
  3. The format of the requested credential.
  4. Optional. An object defining additional properties requested by the verifier that apply to the metadata and validity data of the credential. Only id and format are required on a credential query, so leave meta out entirely if you are not constraining anything.
  5. An array of Claim Queries specifying which claims the wallet must present.
  6. A string that serves as an identifier for the claim query. This is an optional field, but it can be useful for keeping track of claims and their values. If used, the value must not conflict with other IDs.
  7. An array of strings specifying a path to the requested claim value within a credential.

Each level of that nesting has a name, and the rest of this page uses them:

  • The DCQL query is the whole object. Its only field is credentials.
  • A credential query is one entry of that array: one credential you are asking for, carrying its own id, format and, optionally, claims, meta and trusted_authorities. Asking for two credentials means writing two credential queries, and the wallet must satisfy every one of them.
  • A claim query is one entry of a credential query's claims: one value you want out of that credential.

Writing the DCQL query

The overall structure of your own DCQL query must be updated and maintained. Therefore, use the example above as a guideline and starting point.

The main part of a DCQL query are the list of Claims Queries that specify the information to be requested from the wallet. Within each Claim Query object, you must define path and optionally an id (recommended). The path is an array of strings specifying a path to where the information is located within the credential.

{
    "id": "dateOfBirth",
    "path": ["credentialSubject", "dateOfBirth"]
}

Add these object blocks into the claims array in the JSON structure separated by a comma.

Creating the verification query

Register the DCQL query with the service by calling POST /verification-query/create. You can also try it from the API specification a running service serves.

The DCQL query goes inside a dcql_query field, alongside an optional uri_prefix:

curl -X 'POST' \
  '{credential_verification_service_URL}/verification-query/create' \
  -H 'Authorization: Bearer {your API key}' \
  -H 'Content-Type: application/json' \
  -d '{
  "dcql_query": {
    "credentials": [
      {
        "id": "studentCard",
        "format": "jwt_vc_json-ld",
        "claims": [
          {
            "id": "studentId",
            "path": ["credentialSubject", "studentId"]
          },
          {
            "id": "dateOfBirth",
            "path": ["credentialSubject", "dateOfBirth"]
          },
          {
            "id": "enrollmentYear",
            "path": ["credentialSubject", "enrollmentYear"]
          }
        ]
      }
    ]
  }
}'

The DCQL query is not the request body

dcql_query is required, and the credential query array goes inside it. Posting the bare { "credentials": [...] } fails.

uri_prefix is the other field this endpoint accepts, and it belongs to the verification query rather than to the DCQL query inside it. It sets the scheme of the deep link that starting a session from this query returns, and defaults to haip-vp://. Set it only to match a wallet that registers a different scheme — the issuer's equivalent is credentialOfferPrefix.

Asking an AltID holder looks different

A query for the Danish AltID app asks for the mso_mdoc format rather than jwt_vc_json-ld, which changes how path is written, adds a required meta.doctype_value, and comes with its own uri_prefix. The whole integration is in Verify credentials from AltID.

The service returns 201 - created, and the response body is the new verification query's id as plain text. Keep it: that id is what you pass as verification_query_id when you request a presentation.

You can read a verification query back with GET /verification-query/{verification_query_id}.

Restricting which issuers you accept

A DCQL query can name the authorities you are willing to accept a credential from. Add a trusted_authorities array to a credential query, and only credentials whose issuer chains up to one of those authorities satisfy it. The field is OID4VP section 6.1.1, Trusted Authorities Query.

The array belongs to a credential query, so what follows is one entry of the credentials array rather than a whole DCQL query:

{
  "id": "studentCard",
  "format": "jwt_vc_json-ld",
  "trusted_authorities": [ // (1)!
    {
      "type": "aki", // (2)!
      "values": [ "s9tIpPmhxdiuNkHMEWNpYim8S8Y" ] // (3)!
    }
  ],
  "claims": [
    {
      "id": "studentId",
      "path": [ "credentialSubject", "studentId" ]
    }
  ]
}
  1. Optional, and set per credential query. Leave it out entirely to accept any issuer the verifier's trust anchors allow. A credential satisfies the array if it matches any entry in it.
  2. The only type this implementation enforces is aki.
  3. A non-empty list of authority key identifiers. A credential matches if any of them matches, so this is the list of certificate authorities you accept for this credential.

What aki matches

aki is the Authority Key Identifier, the field an X.509 certificate uses to name the key that signed it (RFC 5280 §4.2.1.1). A credential carries its issuer's certificate chain in the x5c header, so the chain says which CA key signed the issuing certificate — and that is what an aki value pins.

The check runs over the whole chain: the keyIdentifier is read from every certificate in the credential's x5c that carries the extension, base64url-encoded without padding, and the credential matches if any of those values appears in any values list of type aki.

Two consequences follow from matching on the identifier rather than on a path:

  • A credential presenting no chain never matches. A did:jwk: issuer has no x5c, so it has no authority key identifier to compare and is excluded by any trusted_authorities entry.
  • The match is an exact string comparison, not a signature check. Nothing about the value proves the chain is genuine; a forged certificate can carry any identifier its author likes. What makes the chain trustworthy is PKIX path validation against your trust anchors, which is a separate, deployment-wide setting. Treat trusted_authorities as narrowing an already-validated set of issuers, never as a replacement for configuring anchors.

Finding the value to use

Read the identifier off a credential's issuing certificate — the leaf of the chain the issuer emits, which is the first entry of its certificateFiles:

openssl x509 -in issuer.pem -noout -ext authorityKeyIdentifier
X509v3 Authority Key Identifier:
    B3:DB:48:A4:F9:A1:C5:D8:AE:36:41:CC:11:63:69:62:29:BC:4B:C6

The values entry is those bytes base64url-encoded without padding — the twenty above are the s9tIpPmhxdiuNkHMEWNpYim8S8Y of the example. No OpenSSL option prints that encoding directly, so convert the hex with whatever is at hand, keeping in mind that plain base64 is not interchangeable with it: + and / become - and _, and the trailing = padding is dropped.

The same identifier sits on the CA's own certificate as its Subject Key Identifier, so -ext subjectKeyIdentifier against that certificate prints the same bytes when the CA is what you have rather than a credential.

The quickstart's self-signed certificate has one too

A certificate generated by openssl req -x509 as in Signing certificates is its own authority, and carries an Authority Key Identifier equal to its Subject Key Identifier. So the command above works against a local setup, and you can exercise trusted_authorities end to end before you have a real CA.

What's Next