Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/tessio-eu:tessio-eu-age-verification-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Tessio.Cloud Age Verification API
version: 0.1.0
license:
name: Proprietary
description: 'The Tessio.Cloud integration API: start an age check, read its result, manage the webhook and read your stats. Every operation takes an API key (`tk_…`) as a Bearer token. This is the public part of the full description, generated from it; the guides are at https://tessio.eu/docs/.'
servers:
- url: https://api.tessio.eu
security:
- bearerAuth: []
tags:
- name: Age Verification
paths:
/v1/age-checks:
post:
tags:
- Age Verification
summary: Start an age check
description: '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.'
security:
- bearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: false
description: Unique per project; a retry with the same key returns the original check (nothing is created or metered again). Kept for 24 hours, after which a retry carrying it creates and meters a new check.
schema:
type: string
maxLength: 255
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAgeCheckRequest'
responses:
'403':
$ref: '#/components/responses/InsufficientScope'
'201':
description: Check created (poll the returned id)
headers:
Location:
schema:
type: string
description: The new check's path, `/v1/age-checks/{id}`
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckCreated'
'400':
description: '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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing / invalid / revoked key
'409':
description: idempotency_key_reuse (same key, different body) or idempotency_in_progress (concurrent create)
'429':
description: '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.
'
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait (rate limit)
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: postV1AgeChecks
x-operation-id-source: derived
get:
tags:
- Age Verification
summary: List the project's age checks
description: 'Newest first, keyset paginated, with optional status, method and billable filters. No request url, and no name, date of birth, document or photo from the credential. Several filters narrow to their intersection.
The cursor is a position in one fixed order that every filter shares, so paging on from a check the filter excludes is well defined and continues from where that check sits.'
security:
- bearerAuth: []
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: startingAfter
in: query
description: A check id; returns checks older than it (cursor).
schema:
type: string
format: uuid
- $ref: '#/components/parameters/checkStatus'
- $ref: '#/components/parameters/checkMethod'
- $ref: '#/components/parameters/checkBillable'
responses:
'403':
$ref: '#/components/responses/InsufficientScope'
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckList'
'400':
description: invalid_status / invalid_method
'401':
description: Missing / invalid / revoked key
operationId: getV1AgeChecks
x-operation-id-source: derived
/v1/age-checks/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
tags:
- Age Verification
summary: Poll an age check
description: 'Scoped to a project: a key can only read checks it created. `result` stays null until the check completes.'
security:
- bearerAuth: []
responses:
'403':
$ref: '#/components/responses/InsufficientScope'
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheck'
'401':
description: Missing / invalid / revoked key
'404':
description: Unknown check / not this project's
operationId: getV1AgeChecksById
x-operation-id-source: derived
components:
schemas:
AgeCheckSummary:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
- completed
- expired
- failed
minAge:
type: integer
meetsMinAge:
type:
- boolean
- 'null'
method:
type:
- string
- 'null'
transport:
type:
- string
- 'null'
enum:
- openid4vp
- dcapi
- null
description: 'How a real wallet''s answer travelled, as on `AgeCheckResult`. Null when no wallet was involved, and also null while the check is still pending.
'
reference:
type:
- string
- 'null'
description: Your correlation id from create, echoed back
createdAt:
type: string
format: date-time
mode:
$ref: '#/components/schemas/Mode'
billable:
type: boolean
description: 'Whether this check is on an invoice. NOT derivable from `status`: a failed check may be a presentation that did not verify, which is free, or one that verified without a readable claim, which bills.'
failedAttempts:
type: integer
description: Presentations refused while the check stayed pending. It tells apart an expired check someone tried to answer from one nobody opened.
AgeCheck:
type: object
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
- completed
- expired
- failed
result:
description: Null until status is completed. Stays null when status is failed, which means no answer was obtained.
oneOf:
- type: 'null'
- $ref: '#/components/schemas/AgeCheckResult'
reference:
type:
- string
- 'null'
description: Your correlation id from create, echoed back
mode:
$ref: '#/components/schemas/Mode'
failedAttempts:
type: integer
description: 'How many presentations failed verification, or wallet error responses arrived, while the check stayed pending, on either transport. Neither ends the check, so a pending or expired check with a count above zero means a response was posted and refused, or a wallet said it could not answer. It counts every refused response for the check, not only the holder''s, so read it as a signal rather than as the person''s own attempts.
'
lastFailureCodes:
type:
- array
- 'null'
items:
type: string
description: 'The error codes of the most recent failed attempt, such as `issuer_untrusted` or `signature_invalid`. A code beginning `wallet_` is the wallet''s own error response, for example `wallet_access_denied` when the holder declined, the wallet held no matching credential or it could not authenticate the holder (OpenID4VP 1.0 section 8.5 gives one code for all three): it is the wallet''s claim and nothing was verified. `session_mismatch` is a same device presentation whose redirect came back to a different browser from the one that opened the wallet, rejected under HAIP 1.0 section 5.1. Null when there has been none. Kept after the check completes.
'
AgeCheckCreated:
type: object
properties:
id:
type: string
format: uuid
url:
type: string
format: uri
description: '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:
type: string
enum:
- pending
expiresAt:
type: string
format: date-time
mode:
$ref: '#/components/schemas/Mode'
Mode:
type: string
enum:
- test
- live
description: '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`.
'
Error:
type: object
properties:
error:
type: string
description: Machine readable code (for example invalid_from, insufficient_scope, idempotency_key_reuse)
CreateAgeCheckRequest:
type: object
description: All fields optional. Omit the body to check age_over_18.
properties:
minAge:
type: integer
default: 18
minimum: 1
maximum: 99
description: '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:
type: string
maxLength: 200
description: '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:
type: string
enum:
- proof-of-age
- pid-mdoc
- pid-sdjwt
- pid-de-sdjwt
- photo-id
description: '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.'
AgeCheckList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/AgeCheckSummary'
hasMore:
type: boolean
description: More rows exist; page with ?startingAfter=<last id>.
AgeCheckResult:
type: object
properties:
minAge:
type: integer
meetsMinAge:
type: boolean
description: True if the user proved they meet the age threshold
method:
type: string
enum:
- demo
- simulated
- eudiw
- av
description: What answered the check. 'demo' is a sandbox completion and 'simulated' is the public demo's explicit simulate call, neither of which involves a wallet. 'eudiw' and 'av' name the profile a real wallet presented under.
transport:
type:
- string
- 'null'
enum:
- openid4vp
- dcapi
- null
description: 'How a real wallet''s answer travelled: `openid4vp` for the deeplink flow, `dcapi` for the W3C Digital Credentials API. Null when no wallet was involved, so a sandbox completion stays distinguishable on this field too.
'
parameters:
checkMethod:
name: method
in: query
description: Only checks answered by this profile. `eudiw` and `av` are real wallet presentations. `demo` is the sandbox and `simulated` is the public demo's explicit simulate call, neither of which involves a wallet. Omit for every method, which also includes checks nothing has answered yet.
schema:
type: string
enum:
- eudiw
- av
- demo
- simulated
checkStatus:
name: status
in: query
description: Only checks in this status. Omit for every status.
schema:
type: string
enum:
- pending
- completed
- expired
- failed
checkBillable:
name: billable
in: query
description: 'Only checks that are (true) or are not (false) on an invoice. Omit for both. This cannot be derived from `status`: a failed check may be a presentation that did not verify, which is free, or one that verified without a readable claim, which bills.'
schema:
type: boolean
responses:
InsufficientScope:
description: 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`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: An API key (`tk_…`). A project's key with scope `full` calls every `/v1` route for its project; with `stats:read` it calls `/v1/stats/*` only. An organisation's key is always `stats:read` and reads every project in the organisation.