Docs · API reference
Verifications API
The merchant surface: create a verification, read it, redeem a same-device response code, erase it, and see where the day's quota stands.
Create a verification
POST/v1/verificationsverifications:create
Creates a verification session and mints the wallet-facing
wallet_uri.
Body
| Field | Type | Notes |
|---|---|---|
template | string, 1–64 chars | A built-in (age_over_18, basic_identity) or one of your own template keys. Exactly one of template/query. |
query | object | An explicit DCQL query — { credentials: [...], credential_sets?: [...] } — instead of a template. |
webhook_url | string (URI) | An http(s) endpoint that receives the signed result. Must resolve to a public address; redirects are not followed. |
redirect_url | string (URI) | Where the wallet sends the user after responding, with #response_code=… appended. |
ttl_seconds | integer, 60–3600 | Session lifetime. Default 300. |
metadata | object, string → string | Opaque key/values of your own, echoed back on the result and on the webhook. |
{
"id": "vrf_G3mgaU2NnqZU-vNBd_jFAw",
"status": "pending",
"template": "age_over_18",
"wallet_uri": "openid4vp://?client_id=valitel-dev-verifier&request_uri=...",
"request_uri": "https://api.valitel.eu/wallet/request/vrf_G3mgaU2NnqZU-vNBd_jFAw",
"created_at": "2026-08-14T16:20:43.170Z",
"expires_at": "2026-08-14T16:25:43.170Z"
}wallet_uri and request_uri are present only while
status is pending. A terminal session carries
claims (merged across credentials) and credentials
(per-credential format, trust findings and proof validity) instead.
Errors400 invalid_request 400 invalid_idempotency_key 401 invalid_api_key 401 api_key_expired 403 insufficient_scope 409 idempotency_key_in_flight 422 unknown_template 422 invalid_webhook_url 422 idempotency_key_reused 429 rate_limit_exceeded 429 quota_exceeded 500 internal_error
Retrying safely
Send an Idempotency-Key header (1–255 printable ASCII characters, no
spaces; a UUID is recommended) to make retries of this endpoint safe. Within 24 hours,
resending the identical key and body replays the original 201 — with an
Idempotent-Replayed: true response header — instead of creating a second
session, and spends no quota. Only a successful 201 is ever cached, so a
request that errored can be retried immediately with the same key.
| Situation | Response |
|---|---|
| first use | normal |
| same key, same body, done | original 201 + Idempotent-Replayed |
| same key, same body, still running | 409 idempotency_key_in_flight + Retry-After |
| same key, different body | 422 idempotency_key_reused |
| malformed key | 400 invalid_idempotency_key |
| after 24 h | treated as new |
List verifications
GET/v1/verificationsverifications:read
Lists your own sessions, newest first.
| Query parameter | Type | Notes |
|---|---|---|
limit | integer, 1–100 | Default 20. |
before | ISO 8601 datetime | Sessions created strictly before this cursor — page by passing the oldest created_at you have seen. |
{ "data": [ /* same shape as the create response */ ] }Errors400 invalid_request 401 invalid_api_key 403 insufficient_scope 429 rate_limit_exceeded 500 internal_error
Retrieve a verification
GET/v1/verifications/:idverifications:read
Fetches one session, scoped to your organisation.
{
"id": "vrf_G3mgaU2NnqZU-vNBd_jFAw",
"status": "completed",
"template": "age_over_18",
"claims": { "age_over_18": true },
"credentials": [
{
"format": "mso_mdoc",
"doctype_or_vct": "eu.europa.ec.av.1",
"claims": { "age_over_18": true },
"issuer": { "common_name": "Test Document Signer", "country": "UT" },
"trusted": true,
"signature_valid": true,
"holder_binding_valid": true,
"findings": [
{
"check": "san_match",
"status": "skipped",
"detail": "SAN matching applies to the sdjwt-issuer profile only"
}
]
}
],
"created_at": "2026-08-14T16:20:43.170Z",
"expires_at": "2026-08-14T16:25:43.170Z",
"responded_at": "2026-08-14T16:20:47.117Z"
}Errors401 invalid_api_key 403 insufficient_scope 404 session_not_found 429 rate_limit_exceeded 500 internal_error
Poll a status (no key)
GET/v1/verifications/:id/statusunauthenticated
The poll the widget makes from your own page. No bearer token, and deliberately limited to two fields:
{ "id": "vrf_G3mgaU2NnqZU-vNBd_jFAw", "status": "completed" }A browser holding only the session id must never be able to read verified claims
through this route, so it does not return them. It sets
access-control-allow-origin: *; a credential-less simple
GET never preflights, so that one header is the entire CORS story, and
no other route is cross-origin readable.
Errors404 session_not_found 429 rate_limit_exceeded 500 internal_error
Redeem a response code
POST/v1/verifications/:id/redeemverifications:read
The same-device analogue of polling. When a session was created with
redirect_url, the wallet returns the user to it with
#response_code=… appended; redeem that code for the result.
Single-use.
{ "response_code": "<string, min 16 chars>" }Errors400 invalid_request 401 invalid_api_key 403 insufficient_scope 404 invalid_response_code 429 rate_limit_exceeded 500 internal_error
Erase a verification
DELETE/v1/verifications/:idverifications:delete
Erases one verification and everything derived from it: the session, its result and its webhook delivery log — the three places a disclosed claim can be.
{
"id": "vrf_G3mgaU2NnqZU-vNBd_jFAw",
"deleted": {
"verification_sessions": 1,
"verification_results": 1,
"webhook_deliveries": 2
},
"erased_at": "2026-08-15T10:00:00.000Z"
}The counts are measured, not inferred, so an Article 17 erasure request can be
answered with numbers rather than an assurance. A 0 means there was
genuinely nothing there — a pending session has no result, and a verification with no
webhook_url has no deliveries.
The operation is deliberately not idempotent: a second DELETE of the
same id is a 404, because a 200 would confirm the id had
once existed. An erasure appends an audit event carrying the counts and the
timestamp; audit rows never hold claim values, which is what makes keeping that
record compatible with the erasure it describes.
Errors401 invalid_api_key 403 insufficient_scope 404 session_not_found 429 rate_limit_exceeded 500 internal_error
Usage and quota
GET/v1/usageverifications:read
Every account has a daily budget of verification creations — 250/day on a test key and 10,000/day on a live one by default — which resets at midnight UTC. Creating a verification spends one unit; nothing else on the API spends anything, this endpoint included.
{
"day": "2026-08-17",
"verifications_created": 42,
"daily_quota": 250,
"remaining": 208,
"recent": [
{ "day": "2026-08-17", "verifications_created": 42 },
{ "day": "2026-08-16", "verifications_created": 191 }
]
}remaining is floored at zero, so it reads 0 rather than a
negative number if the budget is lowered below what has already gone out today.
recent lists the days with any usage at all, newest first, up to 30 of
them — a day with no creations has no row.
Once the budget is spent, creating answers 429 with code
quota_exceeded and a Retry-After header carrying the
seconds until the reset. Branch on the code rather than the status:
rate_limit_exceeded means slow down and retry the same request, while
quota_exceeded means retrying before midnight UTC cannot succeed.
Polling this endpoint is how you stay ahead of that rather than discovering the
ceiling by hitting it.
Errors401 invalid_api_key 403 insufficient_scope 429 rate_limit_exceeded 500 internal_error