{status === 'initializing' &&
Starting verification...
}
{status === 'waiting' && (
<>
Verify Your Credentials
{qrCode && (

)}
Scan with your wallet app
{deepLink && (
Open Wallet
)}
>
)}
{status === 'success' && (
Verification successful
)}
{status === 'error' && (
Verification failed
)}
);
}
```
## Best Practices
### Security
1. **Always use HTTPS** for your callback URLs
2. **Validate webhook signatures** if your implementation supports them
3. **Use short session TTLs** (10 minutes is recommended)
4. **Clean up sessions** after completion to prevent replay attacks
### User Experience
1. **Show status updates** as the wallet interacts with the request
2. **Provide a deep link** for same-device flows
3. **Handle timeouts gracefully** with clear messaging
4. **Allow retry** if verification fails
### Performance
1. **Use webhooks** instead of polling when possible
2. **Cache access tokens** to avoid repeated OAuth flows
## Troubleshooting
### Common Issues
| Issue | Cause | Solution |
|-------|-------|----------|
| QR code not scanning | Wrong URL scheme | Ensure wallet supports `openid4vp://` |
| Wallet shows error | Invalid query configuration | Contact administrator |
| Timeout | User abandoned flow | Implement retry mechanism |
| No verified data | Wrong status check | Poll until `authorization_response_verified` |
### Debug Mode
Enable verbose logging to diagnose issues:
```kotlin
// In your application configuration
sphereon.oid4vp.debug = true
```
## Next Steps
- **[OID4VP Verifier REST API](/edk/rest-apis/verifiable-credentials/oid4vp-verifier)** - Full OpenAPI documentation
- **[OID4VP Overview](./overview)** - Architecture and concepts
---
# DCQL Store
# DCQL Store
The IDK ships a `DcqlQueryConfigurationStore` interface and a default key-value-backed implementation that is fine for tests and single-tenant developer setups but loses history on every write. The EDK replaces it with `VersionedDcqlQueryConfigurationStore`: same contract for ordinary reads and writes, plus an immutable per-query version trail backed by a relational database. Every write produces a new immutable row in a shared `config_version` table and advances a current-version pointer in a typed `dcql_query` header table. Reads through the IDK interface continue to return the body the current-version pointer references, so existing IDK-facing code keeps working without changes. The version-history operations are exposed as a small additional REST surface alongside the existing IDK DCQL admin endpoints.
## Storage Model
The store keeps two related rows for every DCQL query:
The `dcql_query` table holds the **mutable header** keyed by `(tenant_id, identifier)`. It stores the logical query identity (`name`, `description`, `enabled`), the current-version pointer (`current_version`), a soft-delete timestamp (`deleted_at`, omitted from normal reads), and `created_at` / `updated_at` audit columns. Updates to header metadata happen in place; only the DCQL body is versioned.
The shared `config_version` table holds the **immutable history**. Each row carries the full serialized DCQL body for one version of one query, plus `created_at` and `created_by` audit columns. The `config_version` table is also used by other versioned configuration types in the EDK (credential designs, for example), so the same versioning infrastructure is reused; the `domain` column on each row distinguishes them (`"oid4vp-dcql"` for DCQL).
```kotlin
data class DcqlQueryHeaderRow(
val tenantId: String,
val identifier: String,
val name: String,
val description: String? = null,
val enabled: Boolean = true,
val currentVersion: Int,
val createdAt: Long,
val updatedAt: Long,
)
data class DcqlQueryVersionRecord(
val identifier: String,
val version: Int,
val dcqlQuery: DcqlQuery,
val createdAt: Long,
val createdBy: String? = null,
)
data class DcqlQueryVersionSummary(
val version: Int,
val createdAt: Long,
val createdBy: String? = null,
)
```
Tenant isolation is enforced at the query layer: every repository method filters by `tenantId`, which is resolved from the session context, not from a wire-supplied argument. The store has no concept of a verifier; that's a layer above, handled by the [verifier-DCQL binding](./verifier-bindings) system.
## What Counts as a New Version
Every write through the IDK `DcqlQueryConfigurationStore.put`/`putPersistent` path appends a new immutable `config_version` row and advances the header's `currentVersion`. Translated to the HTTP surface:
- `POST /api/v1/oid4vp/dcql` writes version 1 along with the header.
- `PUT /api/v1/oid4vp/dcql/{queryId}` appends a new version.
- `PATCH /api/v1/oid4vp/dcql/{queryId}` also appends a new version, even when only `name`, `description`, or `enabled` is touched. The version row carries the DCQL body; for a header-only PATCH the new row's body is identical to the previous one. This is a deliberate trade-off: the version history is monotonic and every admin action shows up as a row, at the cost of "no-op" body rows.
- `POST /api/v1/oid4vp/dcql/{queryId}/versions/{version}/restore` appends a new version whose body equals the chosen historical version and advances the pointer. History rows are never mutated.
Deletes are soft: the header's `deleted_at` is set and the row stops appearing in normal reads. The version history is preserved internally even though the version-history endpoints return `404` for soft-deleted queries.
## REST API
The full HTTP surface for managing DCQL queries and walking version history is documented on its own page, with request and response shapes per endpoint, error mapping, tenancy rules, and policy considerations: see [DCQL REST API](./dcql-rest-api). In brief, the basic CRUD endpoints come from the IDK `DcqlQueryAdminHttpAdapter` (`lib-oid4vp-dcql-store-rest`) and the version-history endpoints come from the EDK `DcqlQueryVersionHttpAdapter` (`lib-oid4vp-dcql-store-versioned-rest`); both mount under `/api/v1/oid4vp/dcql` and share the `oid4vp-dcql` OpenAPI tag.
## Choosing a Backend
Two persistence backends are shipped. PostgreSQL is the recommended default for new deployments; MySQL is provided for organisations that have standardised on MySQL.
| Module | Backend |
|--------|---------|
| `lib-oid4vp-dcql-store-versioned-persistence-postgresql` | PostgreSQL via SQLDelight |
| `lib-oid4vp-dcql-store-versioned-persistence-mysql` | MySQL via SQLDelight |
Both backends share the same SQLDelight-generated repository surface (`DcqlQueryHeaderRepository`), so the rest of the EDK does not care which one is wired in. Include exactly one of the persistence modules; the version-history rows go into the shared `config_version` table that the EDK provides as part of its versioned-config infrastructure, so no additional setup is needed.
The repositories run inside the same multi-tenant database routing layer the rest of the EDK uses, so per-tenant database selection and shared-schema-with-tenant-column deployments both work without DCQL-specific configuration.
## Resolution at Authorization-Request Creation Time
When the universal OID4VP backend (`POST /oid4vp/backend/auth/requests`) creates an authorization request for a given `query_id`, it does not embed the DCQL body inline. It resolves the body through the registered `DcqlQueryResolver`, which by default in the EDK is the version-aware resolver (`EdkDcqlQueryResolver`) and, when the VDX binding module is on the classpath, the verifier-aware resolver (`VdxDcqlQueryResolver`). The resolver picks the body from the versioned store, snapshots both the `queryId` and the `version` into the authorization session, and uses that pair to render the DCQL body in the authorization request the wallet eventually fetches. The version is part of the snapshot, so a DCQL update committed after the session was created does not affect the wallet's view of the request.
## Audit
Every write through the versioned store and every version-history endpoint goes through the EDK `PolicyCommandExtension`, so [authorization](/edk/guides/authorization/overview) policies can scope DCQL admin and history operations independently. The standard EDK [audit](/edk/guides/audit/overview) pipeline records every command execution; the `createdBy` column on the version row carries the actor for the data-side audit. Together, the audit event timestamp and the version row's audit columns let an investigator answer both "who did this" and "what did the DCQL body look like at any past moment".
---
# DCQL REST API
import ApiCapability from '@site/src/components/ApiCapability';
# DCQL REST API
The DCQL query configurations that the [Universal OID4VP API](./integration) references by `query_id` are managed through a REST surface mounted under `/api/dcql/v1`. 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](./dcql-store) page for the storage model (header + immutable `config_version` rows) and the semantics of "current version".
## Quick Reference
### Admin Endpoints (IDK)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/dcql/v1/queries` | List every DCQL query configuration in the tenant |
| POST | `/api/dcql/v1/queries` | Create a new DCQL query configuration |
| GET | `/api/dcql/v1/queries/{queryId}` | Read one DCQL query configuration |
| PUT | `/api/dcql/v1/queries/{queryId}` | Replace a DCQL query configuration in full |
| PATCH | `/api/dcql/v1/queries/{queryId}` | Partially update a DCQL query configuration |
| DELETE | `/api/dcql/v1/queries/{queryId}` | Delete a DCQL query configuration |
### Version Endpoints (EDK, present when the versioned store is wired)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/dcql/v1/queries/{queryId}/versions` | List the version history of a DCQL query |
| GET | `/api/dcql/v1/queries/{queryId}/versions/{version}` | Read the body of one historical version |
| POST | `/api/dcql/v1/queries/{queryId}/versions/{version}/restore` | Append 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](./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
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.