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

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 */ ] }

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"
        }
      ]
    }
  ],
  "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:

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.

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