Skip to main content
Version: v0.25.0 (Latest)

DCQL REST API

The DCQL query configurations that the Universal OID4VP API references by query_id are managed through a REST surface mounted under /api/v1/oid4vp/dcql. The basic CRUD operations come from the IDK DcqlQueryAdminHttpAdapter and work against any DCQL store (the in-memory IDK default or the EDK versioned store). When the EDK versioned store is wired in, a second adapter, the DcqlQueryVersionHttpAdapter, mounts the version-history endpoints alongside the admin endpoints under the same base path.

Both adapters are wired automatically once the relevant module is on the classpath of the host service. The IDK admin endpoints come from lib-oid4vp-dcql-store-rest. The EDK version endpoints come from lib-oid4vp-dcql-store-versioned-rest. They share the OpenAPI tag oid4vp-dcql and the operation-id prefix oid4vpDcql, so tools like Swagger UI present them as one logical group.

This page assumes you have read the DCQL Store page for the storage model (header + immutable config_version rows) and the semantics of "current version".

Quick Reference

Admin Endpoints (IDK)

MethodPathDescription
GET/api/v1/oid4vp/dcqlList every DCQL query configuration in the tenant
POST/api/v1/oid4vp/dcqlCreate a new DCQL query configuration
GET/api/v1/oid4vp/dcql/{queryId}Read one DCQL query configuration
PUT/api/v1/oid4vp/dcql/{queryId}Replace a DCQL query configuration in full
PATCH/api/v1/oid4vp/dcql/{queryId}Partially update a DCQL query configuration
DELETE/api/v1/oid4vp/dcql/{queryId}Delete a DCQL query configuration

Version Endpoints (EDK, present when the versioned store is wired)

MethodPathDescription
GET/api/v1/oid4vp/dcql/{queryId}/versionsList the version history of a DCQL query
GET/api/v1/oid4vp/dcql/{queryId}/versions/{version}Read the body of one historical version
POST/api/v1/oid4vp/dcql/{queryId}/versions/{version}/restoreAppend a copy of a historical version as the new current version

All endpoints require an OIDC bearer token. The tenant is resolved by the EDK Layer 1 tenant pipeline from the validated JWT, the Host header, or the configured default; it is never read from a client-supplied header. Responses are application/json with Cache-Control: no-store.

Authoring a DCQL Query

Before walking the endpoint shapes it helps to know what goes inside a query. A DcqlQueryConfiguration carries the operator-facing metadata plus an embedded DcqlQuery body. The body is the OpenID4VP-spec DCQL JSON (credentials and credential_sets) that the wallet eventually sees. The wrapper adds the queryId (the stable identifier query_id callers reference), name, description, and enabled.

If you do not want to compose the DCQL body by hand, the EDK ships the semantic-to-DCQL authoring helper that takes a list of semantic-attribute selections and produces the body for you. The authoring helper output is what you would POST as the dcqlQuery field below.

Create

POST /api/v1/oid4vp/dcql
Authorization: Bearer <token>
Content-Type: application/json
{
"queryId": "age_verification",
"name": "Age verification (over 18)",
"description": "Used by the checkout flow to confirm the customer is over 18.",
"enabled": true,
"dcqlQuery": {
"credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["https://issuer.example.com/identity"] },
"claims": [
{ "path": ["age_over_18"] }
]
}
]
}
}

Returns 201 Created with the full DcqlQueryConfiguration (server-assigned createdAt, updatedAt, and on the EDK versioned store, currentVersion = 1).

Errors:

  • 409 Conflict with ALREADY_EXISTS_ERROR if a query with that queryId already exists in the tenant.
  • 400 Bad Request with ILLEGAL_ARGUMENT_ERROR for a malformed body.

Read

GET /api/v1/oid4vp/dcql/age_verification
Authorization: Bearer <token>

Returns 200 OK with the current DcqlQueryConfiguration:

{
"queryId": "age_verification",
"name": "Age verification (over 18)",
"description": "Used by the checkout flow to confirm the customer is over 18.",
"enabled": true,
"dcqlQuery": { "credentials": [ ... ] },
"createdAt": 1747526400000,
"updatedAt": 1747612800000,
"currentVersion": 4
}

createdAt and updatedAt are epoch milliseconds. currentVersion is present when the versioned store is in use and null for the IDK in-memory backings.

404 Not Found with NOT_FOUND_ERROR if no query with that queryId exists.

List

GET /api/v1/oid4vp/dcql
Authorization: Bearer <token>

Returns 200 OK with a JSON array of every DcqlQueryConfiguration in the current tenant. There is no filtering or pagination at this endpoint; if your tenant has many queries, the list is the full set.

Update (PUT vs PATCH)

Both PUT and PATCH accept the same body shape and merge it into the stored configuration. The semantic difference is intent, not server behaviour: convention says a PUT supplies every field, a PATCH supplies a subset and leaves others unchanged. The actual merge rule is the same in both methods: a null (or omitted) field leaves the stored value unchanged.

PATCH /api/v1/oid4vp/dcql/age_verification
Authorization: Bearer <token>
Content-Type: application/json
{
"enabled": false
}

Returns 200 OK with the updated DcqlQueryConfiguration. In the EDK versioned store, the call also appends a new version row and advances currentVersion, even for header-only changes that do not touch dcqlQuery. Each version row carries the DCQL body at the time of the write; the header fields (name, description, enabled) are not part of the versioned payload.

This is intentional: every write produces a complete (queryId, version) snapshot of the body, so the version history is monotonic and a header-only edit is a no-op in body terms but still produces an audit row. If you are concerned about version-table growth, batch your edits.

Errors:

  • 404 Not Found if the query does not exist.
  • 400 Bad Request for malformed body.

Delete

DELETE /api/v1/oid4vp/dcql/age_verification
Authorization: Bearer <token>

Returns 204 No Content on success.

404 Not Found if the query does not exist.

On the EDK versioned store the delete is a soft-delete: the header row's deleted_at is set and the header stops appearing in normal reads, but the config_version history rows are preserved. The history can still be retrieved through the version endpoints if needed for an audit; the query is otherwise inaccessible.

Version History

The endpoints in this section are only present when lib-oid4vp-dcql-store-versioned-rest is on the classpath. On the IDK default in-memory store they return 404.

List Versions

GET /api/v1/oid4vp/dcql/age_verification/versions
Authorization: Bearer <token>

Returns 200 OK with a JSON array of DcqlQueryVersionSummary:

[
{ "version": 4, "createdAt": 1747612800000, "createdBy": "admin@example.com" },
{ "version": 3, "createdAt": 1747600000000, "createdBy": "ci@example.com" },
{ "version": 2, "createdAt": 1747590000000, "createdBy": "admin@example.com" },
{ "version": 1, "createdAt": 1747526400000, "createdBy": "admin@example.com" }
]

Versions are returned newest first. createdBy is the session principal id at write time; it is null when the write was unauthenticated (typically only in test harnesses).

The summary intentionally omits the DCQL body; fetch each version individually if you need it, so an audit UI can render the list without loading possibly-large bodies.

404 Not Found if the query does not exist (including soft-deleted queries, even though their version rows are preserved internally).

Get One Version

GET /api/v1/oid4vp/dcql/age_verification/versions/3
Authorization: Bearer <token>

Returns 200 OK with the full DcqlQueryVersionRecord:

{
"identifier": "age_verification",
"version": 3,
"dcqlQuery": { "credentials": [ ... ] },
"createdAt": 1747600000000,
"createdBy": "ci@example.com"
}

dcqlQuery is the body as it was at that point in time, deserialised from the config_version row. The header metadata (name, description, enabled) is not part of the version record; only the body is versioned. If you need to diff a historical body against the current one, fetch this and GET /api/v1/oid4vp/dcql/{queryId} separately.

Errors:

  • 404 Not Found with NOT_FOUND_ERROR if the query does not exist, or if {version} does not exist for that query.
  • 400 Bad Request with ILLEGAL_ARGUMENT_ERROR if {version} is not a positive integer.

Restore a Version

POST /api/v1/oid4vp/dcql/age_verification/versions/3/restore
Authorization: Bearer <token>
Content-Type: application/json
{
"note": "Reverting after incident INC-2841"
}

The request body is optional. An empty body or {} is equivalent to { "note": null }. The note is carried with the audit event for the restore command; it is not stored on the resulting version row, so do not rely on it as the long-term audit trail (use the standard EDK audit log for that).

Returns 200 OK with the resulting DcqlQueryConfiguration (now reflecting the restored content; currentVersion is the new version number, not 3).

Behaviour: the server reads the body of version 3, appends it as a new version row with the next sequential version number, advances currentVersion, and returns the new state. The historical rows (including the original version 3) are unchanged. Subsequent GET /api/v1/oid4vp/dcql/age_verification returns the restored body; subsequent GET /api/v1/oid4vp/dcql/age_verification/versions lists the new version at the top with version greater than every previous version.

Errors:

  • 404 Not Found if the query or the specified version does not exist.
  • 400 Bad Request if {version} is not a positive integer or the request body is malformed.

Tenancy and Authorization

Every endpoint operates in a single tenant resolved by the EDK Layer 1 tenant pipeline before the request reaches the adapter. A wire-side X-Tenant-Id header may be set for observability but is never consulted for tenant resolution or access control. The implications are the same as for the credential design REST API: calls without a tenant-resolvable JWT fail before the DCQL adapter sees them, every read is implicitly scoped to the caller's tenant, and writes land in the caller's tenant regardless of any tenant-shaped field in the payload.

All endpoints pass through the EDK PolicyCommandExtension, so authorization policies can be applied per command. Typical policies grant reads (list, get, list-versions, get-version) to any authenticated principal and restrict writes (create, replace, patch, delete, restore-version) to administrators. The action names match the underlying command IDs (oid4vp.dcql.http.* for the adapters in this surface), which is what the policy engine keys on.

Error Envelope

Failures use the standard EDK IdkError envelope:

{
"code": "NOT_FOUND_ERROR",
"message": "DCQL query configuration not found: age_verification"
}

Mapping from the codes you will see on this surface to HTTP statuses:

Error codeHTTP statusWhen
ILLEGAL_ARGUMENT_ERROR400Malformed body, non-integer version, bad {queryId}
NOT_FOUND_ERROR404Missing query, missing version
ALREADY_EXISTS_ERROR409Create with an existing queryId
UNKNOWN_ERROR or other500Unexpected server failure

Authorization failures from the PolicyCommandExtension are 403 with the policy decision in the message; authentication failures are handled by the surrounding bearer-token middleware and never reach the adapter.

Resolution Snapshots Are Independent of the Admin API

A wallet that fetches an authorization request does not go through this REST surface. The Universal OID4VP backend calls the in-process DcqlQueryResolver, which reads the body directly from the versioned store and snapshots (queryId, version) into the authorization session at the moment the session is created. Subsequent edits through the admin API do not retroactively change the body the wallet sees, because the wallet's view is pinned by the session snapshot. See DCQL Store for the resolution-time detail.

If you want different verifiers in the same tenant to use the same query_id at different versions, layer the verifier bindings module on top: that adds a per-verifier pinnedVersion and its own REST surface for managing per-verifier pins and scheduled future activations.