Get started

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

FieldTypeNotes
templatestring, 1–64 charsA built-in (age_over_18, basic_identity) or one of your own template keys. Exactly one of template/query.
queryobjectAn explicit DCQL query — { credentials: [...], credential_sets?: [...] } — instead of a template.
webhook_urlstring (URI)An http(s) endpoint that receives the signed result. Must resolve to a public address; redirects are not followed.
redirect_urlstring (URI)Where the wallet sends the user after responding, with #response_code=… appended.
ttl_secondsinteger, 60–3600Session lifetime. Default 300.
require_response_codebooleanSame-device response-code binding (ADR-0049): a completed result parks at awaiting_redeem — no claims, credentials or assurance anywhere — until you redeem the code. Valid only alongside redirect_url (422 response_code_requires_redirect_url otherwise). Default false, and existing integrations that never set it see byte-identical behaviour.
metadataobject, string → stringOpaque key/values of your own, echoed back on the result and on the webhook.
201 Created
{
  "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 completed session carries claims (merged across credentials) and credentials (per-credential format, trust findings and proof validity) with the disclosed values populated; a failed or expired session returns the same shape with every claim value withheld (fail-closed) — only the non-claim evidence (trust findings, proof validity) and, for a failed session, error are populated.

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 response_code_requires_redirect_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.

SituationResponse
first usenormal
same key, same body, doneoriginal 201 + Idempotent-Replayed
same key, same body, still running409 idempotency_key_in_flight + Retry-After
same key, different body422 idempotency_key_reused
malformed key400 invalid_idempotency_key
after 24 htreated as new

List verifications

GET/v1/verificationsverifications:read

Lists your own sessions, newest first.

Query parameterTypeNotes
limitinteger, 1–100Default 20.
beforeISO 8601 datetimeSessions created strictly before this cursor — page by passing the oldest created_at you have seen.
200 OK
{ "data": [ /* same shape as the create response */ ] }

The list never carries claims and never consumes the one-time hand-over.

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.

200 OK
{
  "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"
        }
      ]
    }
  ],
  "assurance": { "revocation": "checked", "credential_status": "absent" },
  "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

Result error codes

These are distinct from the HTTP-level errors above: a failed session still returns 200 OK, with the reason in the result's own error.code.

CodeMeaning
missing_credentialsThe wallet presented no credentials at all.
unsupported_credential_formatEvery presented credential used a format this platform accepts structurally but does not yet verify.
credential_verification_failedA presented credential failed signature, trust-chain or holder-binding verification.
credential_query_mismatchA presented credential did not satisfy the DCQL query — error.message names the query id and reason (there is no separate mismatches field).
response_code_not_redeemedADR-0049: require_response_code was set and the code was never redeemed within the window — see status: "awaiting_redeem" and Redeem a response code.
internal_errorOurs. Retry, and tell us if it persists.

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:

200 OK
{ "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.

Body
{ "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.

Valitel does not store the content of a verification, so on a normal account erasure removes the outcome metadata (status, error code, timestamps, delivery log) and, if the content has not yet been handed over, the sealed copy still held for it, which is made unreadable at the same moment.

200 OK
{
  "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.

200 OK
{
  "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