Developer docs

The age verification API

Start a check, send the user to the EU age verification app or their EU Digital Identity Wallet, and get a signed over 18 result back. You never handle a document, and we store no personal data. Everything below works today against the sandbox, and against the age verification app in production.

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

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_live_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 an OpenID4VP url. Present the url to the user's wallet, as a QR code on desktop or a deep link on mobile.

{
  "id": "3f2a9c7e-8b1d-4e2a-9c11-6a0f2b7d4e88",
  "url": "openid4vp://?client_id=...&request_uri=...",
  "status": "pending",
  "expiresAt": "2026-07-31T17:12:00Z"
}

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_live_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. 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_live_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" }.

No personal data

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 personal data. 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 →