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"
}
]
}
],
"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 is the single bit that means every trust check passed.
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.
4. Or have the result delivered
Polling is fine for a demo. In production, create the verification with a
webhook_url and Valitel posts the same payload to you the moment the
session reaches a terminal state, signed with your webhook secret.
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>.