Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Tessio.Cloud Stats 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: Stats
paths:
/v1/stats/summary:
get:
tags:
- Stats
summary: Outcomes, rates and time to result for a period, beside the period before it
description: Exactly the days asked for, and the period of equal length just before them, so a client can show a change without a second request. Rates are over the checks that ended (passed, under age, failed, expired), so a check still pending moves no rate; each is null when nothing ended. `p50Seconds` and `p95Seconds` run from creation to completion, over completed checks only. `retention` says from which day the plan keeps every check. Under a project's key, over its project; under an organisation's key, over every project in it, or one with `project_id`. UTC days. Any key may call it, of either scope; each key spends its own budget.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/rangeFrom'
- $ref: '#/components/parameters/rangeTo'
- $ref: '#/components/parameters/statsMode'
- $ref: '#/components/parameters/statsProject'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckStatsSummary'
'400':
$ref: '#/components/responses/InvalidStatsRequest'
'401':
description: No key, or one that is unknown or revoked
'403':
$ref: '#/components/responses/PlanFeatureUnavailable'
'429':
description: 'rate_limited: this key''s budget is spent.'
headers:
Retry-After:
description: Seconds until a request can succeed
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1StatsSummary
x-operation-id-source: derived
/v1/stats/timeseries:
get:
tags:
- Stats
summary: One metric over time, with zeros for empty buckets
description: One point per bucket from `from` to `to`, a bucket with no checks included as zero, so a chart never has to fill gaps. The granularity follows the range as on the Stats page, except that a range of two days or less is cut into hours, labelled `YYYY-MM-DDTHH:00Z` and ending at the current hour. Weeks and months are UTC calendar steps. `pass_rate` is null for a bucket where nothing ended. Under a project's key, over its project; under an organisation's key, over every project in it, or one with `project_id`. UTC days. Any key may call it, of either scope; each key spends its own budget.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/rangeFrom'
- $ref: '#/components/parameters/rangeTo'
- $ref: '#/components/parameters/statsMode'
- $ref: '#/components/parameters/statsMetric'
- $ref: '#/components/parameters/statsProject'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckStatsSeries'
'400':
$ref: '#/components/responses/InvalidStatsRequest'
'401':
description: No key, or one that is unknown or revoked
'403':
$ref: '#/components/responses/PlanFeatureUnavailable'
'429':
description: 'rate_limited: this key''s budget is spent.'
headers:
Retry-After:
description: Seconds until a request can succeed
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1StatsTimeseries
x-operation-id-source: derived
/v1/stats/failures:
get:
tags:
- Stats
summary: Failed presentations by reason category
description: 'Every refused presentation in the period, counted by the category of its reason, read from the check timelines. A check can be refused more than once before it ends, so this counts attempts, not checks. The plan keeps timelines for less time than checks: before `retention.eventsFrom` the counts are partial. `group_by` splits each category further. Under a project''s key, over its project; under an organisation''s key, over every project in it, or one with `project_id`. UTC days. Any key may call it, of either scope; each key spends its own budget.'
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/rangeFrom'
- $ref: '#/components/parameters/rangeTo'
- $ref: '#/components/parameters/statsMode'
- $ref: '#/components/parameters/statsFailureGroup'
- $ref: '#/components/parameters/statsProject'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckStatsFailures'
'400':
$ref: '#/components/responses/InvalidStatsRequest'
'401':
description: No key, or one that is unknown or revoked
'403':
$ref: '#/components/responses/PlanFeatureUnavailable'
'429':
description: 'rate_limited: this key''s budget is spent.'
headers:
Retry-After:
description: Seconds until a request can succeed
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1StatsFailures
x-operation-id-source: derived
/v1/stats/funnel:
get:
tags:
- Stats
summary: How far the checks of a period got
description: '`created`, then `responseReceived` (a wallet''s response was judged: refused, answered or held for a browser that never came back), then `completed` (an answer about age came out). The sandbox answers without a wallet, so its checks reach `completed` without `responseReceived`. A held presentation is read from the timeline, so before `retention.eventsFrom` it is not counted. `eudiw` counts the checks that offered an EUDIW request, as their own or as a further one, and of those the ones where a wallet fetched a signed EUDIW request: the step only that profile has. An AV request travels inside its link, so nothing is fetched there and the step does not exist. Under a project''s key, over its project; under an organisation''s key, over every project in it, or one with `project_id`. UTC days. Any key may call it, of either scope; each key spends its own budget.'
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/rangeFrom'
- $ref: '#/components/parameters/rangeTo'
- $ref: '#/components/parameters/statsMode'
- $ref: '#/components/parameters/statsProject'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckStatsFunnel'
'400':
$ref: '#/components/responses/InvalidStatsRequest'
'401':
description: No key, or one that is unknown or revoked
'403':
$ref: '#/components/responses/PlanFeatureUnavailable'
'429':
description: 'rate_limited: this key''s budget is spent.'
headers:
Retry-After:
description: Seconds until a request can succeed
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1StatsFunnel
x-operation-id-source: derived
/v1/stats/breakdown:
get:
tags:
- Stats
summary: Outcomes grouped by a column
description: The same outcome counts as the summary, over exactly the days asked for, one row per value of `group_by`, largest first. `project` and `api_key` rows carry the name as `label`. A deleted key's checks lose their key, so they join the row whose `key` is null. `key` is null for checks with no value in that column, such as a check nothing answered, which has no method. Under a project's key, over its project; under an organisation's key, over every project in it, or one with `project_id`. UTC days. Any key may call it, of either scope; each key spends its own budget.
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/rangeFrom'
- $ref: '#/components/parameters/rangeTo'
- $ref: '#/components/parameters/statsMode'
- $ref: '#/components/parameters/statsBreakdownGroup'
- $ref: '#/components/parameters/statsProject'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/AgeCheckStatsBreakdown'
'400':
$ref: '#/components/responses/InvalidStatsRequest'
'401':
description: No key, or one that is unknown or revoked
'403':
$ref: '#/components/responses/PlanFeatureUnavailable'
'429':
description: 'rate_limited: this key''s budget is spent.'
headers:
Retry-After:
description: Seconds until a request can succeed
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1StatsBreakdown
x-operation-id-source: derived
components:
parameters:
rangeFrom:
name: from
in: query
description: 'The first day, inclusive. A start further back than two years is refused rather than narrowed, so an answer never covers a shorter range than the one asked for.
Omitting both `from` and `to` gives the default window, the last 30 days. Omitting only `from` is a half-written request and is refused.'
schema:
type: string
format: date
statsProject:
name: project_id
in: query
description: Only this project of the org. A project of another org is refused exactly as one that does not exist, `invalid_project_id`. Under a project's key, only that project may be named.
schema:
type: string
format: uuid
statsMetric:
name: metric
in: query
description: What each point counts. Defaults to `checks`, every check created in the bucket.
schema:
type: string
enum:
- checks
- expired
- failed
- pass_rate
- passed
- under_age
default: checks
statsMode:
name: mode
in: query
description: Live checks, test checks or both. Defaults to `live` at org scope (an organisation's key included), which is what a customer's own numbers are, and to the project's own mode at project scope (a project's key included), so a test project reads its checks.
schema:
type: string
enum:
- all
- live
- test
rangeTo:
name: to
in: query
description: The last day, inclusive. Defaults to today, and an end in the future is pulled back to today, so a preset like "this year" does not draw empty months that read as an outage.
schema:
type: string
format: date
statsFailureGroup:
name: group_by
in: query
description: 'Split each category further. `method` is the profile (`av` or `eudiw`) of the request the attempt came through: one of the check''s further requests, or the check''s own, which it carries from creation, for an attempt through it or recorded before attempts named their request. For a check from before profiles were recorded it is the method; what answered a check is written only when it ends, which most refused checks never do. Omit for one row per category.'
schema:
type: string
enum:
- method
statsBreakdownGroup:
name: group_by
in: query
required: true
description: The column to group by.
schema:
type: string
enum:
- api_key
- format
- method
- mode
- project
- transport
schemas:
StatsRetention:
type: object
description: 'The first UTC day the purge has left whole, from what it recorded it may have deleted: after an upgrade the longer plan does not bring back days the shorter one already took. It may understate, never overstate: a day it calls whole is. A range that starts earlier counts only what is still held, so read the older days as partial, not as "no checks". Null where no purge has run for the org, so nothing was deleted.'
properties:
recordsFrom:
type:
- string
- 'null'
format: date
description: 'For the checks: every read.'
eventsFrom:
type:
- string
- 'null'
format: date
description: 'For what is read from each check''s timeline: failures, the funnel''s step for a fetched request under `eudiw`, held presentations (part of the funnel''s `responseReceived`) and `noAnswer`, which leaves out a check whose presentation was held.'
StatsFailureRow:
type: object
properties:
category:
type: string
enum:
- credential_expired
- credential_revoked
- invalid_presentation
- issuer_untrusted
- other
- trust_list_expired
- wallet_declined
- wallet_error
description: Why the presentations were refused, as a category.
group:
type:
- string
- 'null'
description: The value grouped by. Null when not grouped, or when the check has none.
count:
type: integer
format: int64
AgeCheckStatsSummary:
type: object
properties:
mode:
type: string
enum:
- all
- live
- test
current:
$ref: '#/components/schemas/StatsPeriod'
previous:
$ref: '#/components/schemas/StatsPeriod'
retention:
$ref: '#/components/schemas/StatsRetention'
StatsPoint:
type: object
properties:
bucket:
type: string
description: YYYY-MM-DD, or YYYY-MM-DDTHH:00Z for hours (UTC)
value:
type:
- number
- 'null'
AgeCheckStatsBreakdown:
type: object
properties:
mode:
type: string
enum:
- all
- live
- test
groupBy:
type: string
enum:
- api_key
- format
- method
- mode
- project
- transport
rows:
type: array
items:
$ref: '#/components/schemas/StatsBreakdownRow'
retention:
$ref: '#/components/schemas/StatsRetention'
AgeCheckStatsFailures:
type: object
properties:
mode:
type: string
enum:
- all
- live
- test
groupBy:
type:
- string
- 'null'
enum:
- method
- null
rows:
type: array
items:
$ref: '#/components/schemas/StatsFailureRow'
retention:
$ref: '#/components/schemas/StatsRetention'
StatsBreakdownRow:
type: object
properties:
key:
type:
- string
- 'null'
label:
type:
- string
- 'null'
description: The project or API key name, for those groups.
outcomes:
$ref: '#/components/schemas/StatsOutcomes'
StatsPeriod:
type: object
properties:
from:
type: string
format: date
description: First UTC day, inclusive.
to:
type: string
format: date
description: Last UTC day, inclusive.
outcomes:
$ref: '#/components/schemas/StatsOutcomes'
passRate:
type:
- number
- 'null'
description: passed / ended, 0 to 1, where ended is total minus pending. Null when nothing ended.
failRate:
type:
- number
- 'null'
description: (underAge + failed) / ended.
noAnswerRate:
type:
- number
- 'null'
description: noAnswer / ended.
p50Seconds:
type:
- number
- 'null'
description: Median seconds from creation to completion, over completed checks. For a same device check this includes the browser's return, because a check completes when the browser redeems the answer.
p95Seconds:
type:
- number
- 'null'
description: The 95th percentile of the same.
AgeCheckStatsFunnel:
type: object
properties:
mode:
type: string
enum:
- all
- live
- test
created:
type: integer
format: int64
responseReceived:
type: integer
format: int64
completed:
type: integer
format: int64
eudiw:
$ref: '#/components/schemas/StatsEudiwFunnel'
retention:
$ref: '#/components/schemas/StatsRetention'
StatsOutcomes:
type: object
description: Checks created in the period, by where they are now. `passed`, `underAge`, `failed`, `expired` and `pending` add up to `total`; `noAnswer` is a part of `expired`, not a sixth state.
properties:
total:
type: integer
format: int64
passed:
type: integer
format: int64
description: Completed and old enough.
underAge:
type: integer
format: int64
description: Completed and not old enough.
failed:
type: integer
format: int64
description: A presentation verified but carried no readable answer about age.
expired:
type: integer
format: int64
description: 'Ran out of time: no wallet answered, or every answer was refused or never redeemed.'
pending:
type: integer
format: int64
noAnswer:
type: integer
format: int64
description: 'Of `expired`, those no wallet answered at all: no refused attempt and no presentation held for the browser. A held presentation is read from the check''s timeline, which the plan keeps for less time than the check (`retention.eventsFrom`), so before that day such a check is counted here.'
StatsEudiwFunnel:
type: object
properties:
created:
type: integer
format: int64
requestFetched:
type: integer
format: int64
AgeCheckStatsSeries:
type: object
properties:
mode:
type: string
enum:
- all
- live
- test
metric:
type: string
enum:
- checks
- expired
- failed
- pass_rate
- passed
- under_age
granularity:
type: string
enum:
- day
- hour
- month
- week
points:
type: array
items:
$ref: '#/components/schemas/StatsPoint'
retention:
$ref: '#/components/schemas/StatsRetention'
PlanFeatureUnavailable:
type: object
properties:
error:
type: string
enum:
- plan_feature_unavailable
feature:
type: string
enum:
- stats.breakdown
- stats.failures
- stats.funnel
- stats.summary
- stats.timeseries
Error:
type: object
properties:
error:
type: string
description: Machine readable code (for example invalid_from, insufficient_scope, idempotency_key_reuse)
responses:
InvalidStatsRequest:
description: The range is refused as for every chart (invalid_from, invalid_to, range_inverted, range_too_old, range_too_long), or invalid_mode, invalid_metric, invalid_group_by or invalid_project_id. Checked before the plan, so a malformed request is never answered with an upgrade.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PlanFeatureUnavailable:
description: plan_feature_unavailable. The org's plan does not include this read. Every plan reads the summary, Starter adds the time series, Growth and above read all five.
content:
application/json:
schema:
$ref: '#/components/schemas/PlanFeatureUnavailable'
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.