Developer docs
The age verification API
Start a check, send the user to the EU age verification app and get a signed over 18 result back. You never handle a document, and we store no documents or dates of birth. Everything below works today against the sandbox and against the app in production. The EU Digital Identity Wallet follows once our access certificate is issued.
Signup is open and a test project works as soon as you create one. Create an account to mint a project API key, then manage keys and webhooks in the dashboard.
Base URL and auth
All requests go to https://api.tessio.eu over HTTPS. The machine to machine
endpoints authenticate with a project API key (tk_…) as a Bearer token:
Authorization: Bearer tk_your_key Keep the key server side. It is shown once when you create it, and you can revoke it any time from the dashboard.
Every operation, parameter and response is in the API reference, built from the description the API itself serves.
1. Start a check
Create a check. The body is optional; omit it to verify age_over_18.
Send an Idempotency-Key so a retry returns the original check instead of
creating a new one.
curl -X POST https://api.tessio.eu/v1/age-checks \
-H "Authorization: Bearer tk_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{ "minAge": 18, "reference": "order-4821" }'
You get back a check id and a url for the hosted
verification page. Send the person there; the page picks the transport for their browser and device.
{
"id": "3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88",
"url": "https://cloud.tessio.eu/verify/3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88",
"status": "pending",
"expiresAt": "2026-07-31T17:12:00Z",
"mode": "live"
} 2. Get the result
Register a webhook once and let results come to you. Polling
GET /v1/age-checks/{id} stays available as a fallback.
curl https://api.tessio.eu/v1/age-checks/3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88 \
-H "Authorization: Bearer tk_your_key" result is null until
status is completed. Then it carries the boolean you need:
{
"id": "3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88",
"status": "completed",
"result": { "minAge": 18, "meetsMinAge": true, "method": "demo" },
"reference": "order-4821"
} 3. Webhooks
Point us at your endpoint and we return a signing secret once. Omit
events to receive them all.
curl -X PUT https://api.tessio.eu/v1/webhook \
-H "Authorization: Bearer tk_your_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/webhooks/tessio" }'
Each event is a POST with an X-Tessio-Event header and a signed body:
POST /webhooks/tessio
X-Tessio-Event: age_check.completed
X-Tessio-Signature: t=1753894320,v1=9f86d081884c7d659a2feaa0...
{
"id": "3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88",
"type": "age_check.completed",
"status": "completed",
"result": { "minAge": 18, "meetsMinAge": true, "method": "demo" }
}
Verify the signature before trusting an event. Recompute the HMAC over
t + "." + rawBody with your secret and compare it to
v1:
import crypto from "node:crypto";
// header = req.headers["x-tessio-signature"] -> "t=<unix>,v1=<hex>"
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", webhookSecret)
.update(t + "." + rawBody) // sign the exact raw body
.digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); Errors
Errors return a JSON body like { "error": "code" }.
400 unsupported_min_agethe threshold is outside 1 to 99, so it cannot be expressed as anage_over_NNclaim. Inside that range it is accepted but not guaranteed: onlyage_over_18is mandatory in the attestation, so a threshold the holder's credential does not carry returns a check with statusfailedand a nullmeetsMinAge, never a guess. The sandbox demonstrates 18.401the key is missing, invalid or revoked.409 idempotency_key_reusethe same key was used with a different body.429rate limit (default 60 per minute) or monthly quota. On a rate limit, honour theRetry-Afterheader.
4. Stats, from code or from your assistant
The pass rate, the funnel and why attempts were refused are on your Stats page. The same numbers come back from the stats API under a key that can only read them. Connect an AI assistant to the MCP server with the same key and ask it how your checks are doing.
No documents or dates of birth
A check returns an over 18 boolean, not a date of birth or an identity document. There is no selfie and no ID scan, and we keep no documents or dates of birth. If you need the verifier inside your own boundary, the open source Tessio.Verifier self hosts on your own .NET stack.
Ready to build?
Create an account, mint a project API key and integrate against the sandbox straight away.
Create an account →