Get started

Docs · Get started

Quickstart

Create a verification, let a wallet answer it, and read the verified claims — the whole round trip in four steps.

Before you begin

You need an organisation and an API key. Sign in to the dashboard, open Keys, and mint one. Tick verifications:create and verifications:read — enough for this walkthrough.

Keep it out of your shell history
export VALITEL_API_KEY='vk_test_...'

1. Create a verification

Request
curl -s -X POST https://api.valitel.eu/v1/verifications \
  -H "authorization: Bearer $VALITEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{"template":"age_over_18"}'
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 is what you render for the holder. request_uri inside it points at the signed request object; you never fetch that yourself — the wallet does.

2. Show the wallet URI to the holder

Render wallet_uri as a QR code for a cross-device flow, or as a link the holder taps for a same-device flow. The session expires at expires_at — 300 seconds after creation by default, adjustable per verification with ttl_seconds.

If you would rather not build the QR modal and the polling loop, the drop-in widget does both; see the widget below.

3. Read the result

Request
curl -s https://api.valitel.eu/v1/verifications/vrf_G3mgaU2NnqZU-vNBd_jFAw \
  -H "authorization: Bearer $VALITEL_API_KEY"
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"
}

trusted: true means the signature, trust chain and holder-binding proof all checked out at verification time — it is not a live revocation guarantee. findings lists only the checks that did not pass outright — the skipped entry above simply records that SAN matching does not apply to the mdoc profile.

assurance is the separate field for that: revocation is "checked" only when every presented credential's CRL check ran to completion ("degraded" if one was unreachable or failed — including a leaf certificate with no CRL distribution point at all, an unreachable check rather than an uncheckable one — "not_checked" if revocation checking was not configured, or the chain never anchored far enough to attempt one). credential_status covers the IETF Token Status List and mdoc status-list mechanisms — Valitel detects and reports that a credential declares one, but does not fetch or evaluate it today, so this field never reads "checked".

The first read of a completed verification returns the disclosed claims. Valitel does not keep them: later reads return the status only, with no claims, credentials or assurance. Store what you need on that first read.

4. Or have the result delivered

In production, create the verification with a webhook_url and Valitel posts the result to you the moment the session reaches a terminal state, signed with your webhook secret. Every delivery attempt carries the claims until one is answered with a 2xx; once a delivery, a read or a redeem has handed the content over, later deliveries carry the status only. A read that races an in-flight delivery can mean both carry the claims. If every attempt fails (six attempts, about two minutes by default), the content is deleted and a later read returns the status only, so run the verification again.

Request
curl -s -X POST https://api.valitel.eu/v1/verifications \
  -H "authorization: Bearer $VALITEL_API_KEY" \
  -H "content-type: application/json" \
  -d '{"template":"age_over_18","webhook_url":"https://your-server.example/webhook"}'

See Webhooks for the payload, the signature scheme and a reference verifier.

The widget variant

The drop-in widget renders the button, the QR modal and the deep link, and polls the public status endpoint until the session is terminal. It never sees claims — only {id, status}.

HTML
<div id="verify"></div>
<script src="/widget.js"></script>
<script>
  const started = await (await fetch("/checkout/start", { method: "POST" })).json();
  // started = { id, wallet_uri, status_url }

  window.ValitelWidget.mount({
    el: "#verify",
    walletUri: started.wallet_uri,
    statusUrl: started.status_url,
    label: "Verify age",
    onSuccess: () => { /* order placed */ },
    onFailure: (v) => { /* v.status is "failed" | "expired" */ },
    onError: (e) => { /* e.kind — diagnostics, not a verdict */ },
  });
</script>

The modal polls every 1.5 seconds (pollIntervalMs) until a terminal status or a 300-second timeout. ValitelWidget.open is the same modal without the mount-point button, for merchants driving their own trigger. Every class is prefixed valitel- and every element carries a data-valitel-* hook, so the widget never collides with your styles.

onError is diagnostics, not a verdict — it fires for trouble that never reaches onFailure: wallet_link_unresponsive (the deep link produced no app switch), tab_backgrounded (the browser throttled polling while hidden), csp_violation (your CSP blocked the widget's styles or its status poll), timeout_expired, and poll_failing (five consecutive unusable status responses). Each is also written to the browser console whether or not you wire a handler, and every status poll carries the widget build as ?wv=<version>.