---
title: "Start an age check"
method: POST
path: "/v1/age-checks"
tags: ["Age Verification"]
---

# Start an age check

`POST /v1/age-checks`

## Request

- Base URL: `https://api.tessio.eu`
- URL: `https://api.tessio.eu/v1/age-checks`
- Auth: HTTP bearer

The machine to machine surface. Creates an age verification check and returns the `url` of its hosted verification page, to send the person to, plus a check `id` to poll. Rate limited (default 60/min per key). No name, date of birth, document or photo from the credential is stored: the check keeps the boolean age result with your reference. Omit the body to check `age_over_18`. Pass an `Idempotency-Key` header to make create retries safe: a repeated key returns the original check.

**Creating a check does not bill.** A check is charged for when a wallet has presented an attestation and we have verified it, so an abandoned, declined, timed out or unverifiable check costs nothing, and a test mode check never bills at all. What this route does check is the plan's hard ceiling, which exists to bound a runaway integration. A metered plan stops at ten times its included volume, so passing the volume itself is ordinary business, charged nothing per unit; an unmetered plan stops at the bound it reports as both limits.

## Headers

- `Idempotency-Key` string

## Request body

- CreateAgeCheckRequest: All fields optional. Omit the body to check age_over_18.
  - `minAge` integer: Age threshold to verify. 18 is the only value guaranteed to work: the EU age verification attestation makes age_over_18 mandatory at issuance and leaves every other age_over_NN optional, so which further thresholds a holder can answer depends on who issued their credential. Any threshold from 1 to 99 is accepted (the ISO 18013-5 claim is age_over_NN, two digits); one the credential does not carry returns a check with status "failed" and a null meetsMinAge, rather than a guess. The sandbox only ever demonstrates 18.
  - `reference` string: Your correlation id, stored as you send it and returned with the check. Keep it opaque: do not put a name or other personal detail in it. Control characters are refused, including tab, carriage return and newline: the CSV export prints this column, and a newline in it would split one record into two. Printable characters are all accepted, so an id beginning with =, +, - or @ is fine; the export guards those itself.
  - `credential` 'proof-of-age' | 'pid-mdoc' | 'pid-sdjwt' | 'pid-de-sdjwt' | 'photo-id': Which credential the person is asked to present. Omit it for this deployment's default, which is what every check asked for before this field existed. A deployment offers its default plus any credentials it is configured to offer: naming one it does not offer is refused with credential_not_offered, and a name not in this list with unknown_credential. The names are proof-of-age (the EU age verification attestation), pid-mdoc and pid-sdjwt (the EU PID in its two encodings), pid-de-sdjwt (the German PID) and photo-id (the ISO Photo ID). Each check records what it asked for and is judged by that alone.

## Response `201`

Check created (poll the returned id)

- AgeCheckCreated
  - `id` string, uuid
  - `url` string, uri: The hosted verification page for this check: send the person here. Not a wallet deeplink. The page picks a transport for the browser and device in front of it, so a new transport reaches every integration without any of them changing.
  - `status` 'pending'
  - `expiresAt` string, date-time
  - `mode` 'test' | 'live': The mode of the project that created this check, fixed when the project was created. `test` is a sandbox: the check completes on its own after a couple of seconds and its result is simulated, so `meetsMinAge` proves nothing about anyone. `live` means only a wallet presentation completes the check. Read this before acting on `meetsMinAge`.

## Other responses

- `400`: unsupported_min_age: minAge is outside 1 to 99, so it cannot be expressed as an age_over_NN claim. invalid_reference: the reference is longer than 200 characters or contains a control character. invalid_idempotency_key: the Idempotency-Key header is longer than 255 characters or is not well formed text. unknown_credential: credential is not one of the names listed on CreateAgeCheckRequest. credential_not_offered: credential is a real name that this deployment does not offer. All of these are refused before the idempotency key is reserved and before the check is metered, so a refusal costs you neither.
- `401`: Missing / invalid / revoked key
- `403`: insufficient_scope. The key is valid but may only read stats (`stats:read`), so it cannot call this route. Use a project's key with scope `full`.
- `409`: idempotency_key_reuse (same key, different body) or idempotency_in_progress (concurrent create)
- `429`: Rate limited, or the month has reached the plan's hard ceiling (error: quota_exceeded). On a metered plan the ceiling is ten times the included volume, so exceeding what the plan includes is fair use, charged nothing per unit, and is not what this refuses. On an unmetered plan the ceiling and the included volume are the same bound.

---

[API](https://skmtc.dev/tessio/apis/tessio-cloud-api.md) · [All operations](https://skmtc.dev/tessio/apis/tessio-cloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tessio/apis/tessio-cloud-api/revisions/536b3f4769fb?raw)
