tessio-eu Age Verification API

The Age Verification API from tessio-eu — 2 operation(s) for age verification.

Operations 3

POST /v1/age-checks Start an age check #
GET /v1/age-checks List the project's age checks #
GET /v1/age-checks/{id} Poll an age check #

Work with this as data

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 Specification

tessio-eu-age-verification-api-openapi.yml Raw ↑
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.