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.
export VALITEL_API_KEY='vk_test_...'1. Create a verification
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"}'{
"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
curl -s https://api.valitel.eu/v1/verifications/vrf_G3mgaU2NnqZU-vNBd_jFAw \
-H "authorization: Bearer $VALITEL_API_KEY"{
"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.
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}.
<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>.