Technical reference
The contractual detail of the endpoints your application calls directly: response codes, error cases, rate limits. For the exact schema, queryable and always up to date, the Swagger documentation is available at todis.eu/en/docs, no token required.
Authentication and rate limits
Every verification route requires the
Authorization: Bearer <token> header. A
missing, invalid, revoked, or expired token returns
401. Beyond your plan's limit (30 requests/second
on Starter, 60 on Standard), a request returns 429.
When your token is within 7 days of expiring, every
authenticated response also carries an
X-License-Warning header (e.g. license
expires in 3 day(s), renew soon): just a heads-up, the
current request still succeeds normally.
Who decides what gets disclosed
Each attribute of an SD-JWT VC presentation (age, a degree, a status) is protected by its own salted digest in the signed JWT. The wallet only attaches to its presentation the value and salt for the attributes the user agreed to reveal: everything else stays as an irreversible hash. Todis technically only has access to what's actually disclosed in the presentation it receives, never more.
With POST /verify/sessions, the claims
field of your request becomes a DCQL query embedded in the
authorization request sent to the wallet: the user sees and
approves exactly that list before their wallet builds the
presentation. With POST /verify/sd-jwt-vc, that
step has already happened before you obtained the presentation:
Todis verifies it and returns its content as is.
POST /verify/sd-jwt-vc
Verifies an SD-JWT VC presentation. Two modes in the request
body: issuer_public_key_pem (the issuer's key
supplied directly) or country_code (automatic
resolution via the EU's official registry) ;
issuer_public_key_pem takes priority if both are
present.
200: presentation verified,claimspopulated.400: neitherissuer_public_key_pemnorcountry_codesupplied.401: missing, invalid, revoked, or expired license token.422: malformed presentation or signature not verified (verified: false, detail inerror).429: plan request quota exceeded.502: issuer trust registry resolution failed (country_codemode only, e.g. the European registry is temporarily unavailable).
POST /verify/mdoc
Verifies an mdoc presentation (ISO/IEC 18013-5, notably mobile
driving licenses). The trust anchor
(issuer_trust_anchor_pem) must be supplied
explicitly in the request body. It can be the IACA
that issued the document signer, which is the recommended choice:
the signer's chain, embedded in the presentation, is validated up
to it, signature and validity window included, so you do not have
to follow signer rotations. It can also be the signer's own
certificate, if you prefer to pin it. Only one level is followed,
as ISO/IEC 18013-5 intends. There is no automatic per-country
resolution for this format (contact us if a specific mdoc
issuer matters to you).
200: presentation verified,doc_typeandnamespacespopulated.401: missing, invalid, revoked, or expired license token.422: malformed presentation, wrong trust anchor, or signature not verified.429: plan request quota exceeded.
POST /hosted/sessions
The hosted verification journey: this service serves the page
shown to the user itself (QR code, wallet link, copy in French,
English or Spanish). It is the recommended integration when you
would rather not build a verification screen, and it is detailed
on the Integrations page.
You redirect the user to the returned URL, they come back on
your success_url, and you always confirm the result
server-side with GET /verify/sessions/{id}.
success_url means the verification
succeeded, never that the user is of age: a proof of age
that is false lands there too, like any verified
result. Read the result server-side
(claims.age_over_18 with the
age_over_18 shortcut) and apply your own rule to it.
This is the only endpoint that accepts a webhook_url
(a JSON POST signed with HMAC-SHA256 when the journey ends, even
if the user closes the page) and the check
shortcuts ("age_over_18", "identity"),
available on every plan. It requires a deployment with a request
signing identity, which the hosted service has.
The dc_api option. With
"dc_api": true, the hosted page offers the visitor
to open their wallet through the W3C Digital Credentials API,
when their browser supports it; the QR code stays on the page.
The protocol is ISO/IEC 18013-7 Annex C
(org-iso-mdoc): only mdoc credentials
(ISO/IEC 18013-5) go through this path, and a PID in SD-JWT VC
format goes through the QR code. With
check: "age_over_18", this path asks for the proof
of age attestation (eu.europa.ec.av.1), with no
fallback to the PID. The wallet's response is encrypted for Todis
and bound to the origin https://verify.todis.eu:
there is no origin for you to declare. The result,
GET /verify/sessions/{id} and the webhook are
unchanged. Outside a browser, for example in a native app, use
the QR code or the deep link.
201: journey created, withhosted_url,expires_atand the webhook secret.400: invalid return URL, unknowncheck, or inconsistent verification fields.401: missing, invalid, revoked, or expired license token.403: the token's plan does not include the full DCQL grammar (adcql_queryyou supplied).429: plan request quota exceeded.501: deployment without a signing identity, hosted flow unavailable.
POST /verify/sessions
Creates an OpenID4VP verification session. The request body
lists the requested credential type (vct) and the
requested attributes (claims, simple paths, e.g.
"age_over_18"). Trust in the issuer comes from
country_code (SD-JWT VC, the EU's official list) or
from this deployment's trust registries, listed by
GET /trust/registries. The response contains an
OpenID4VP authorization request to present to the user's
wallet (QR code or deep link); how to build the exact QR code or
deep link string is detailed in our
integration file. The session expires 15 minutes
after creation, whether it received a response or not. For
advanced needs (several credentials per session, alternatives
between credentials or between attributes, constrained values),
the dcql_query field accepts a full DCQL query in
place of vct/claims, and can mix SD-JWT
VC and mdoc. The credential_trust field then gives
an SD-JWT VC credential its own country_code, to
verify credentials from issuers of different countries in a
single session (each entry only ever applies to its own
credential, never to another). See the
details in the OpenAPI specification.
encrypted_response defaults to false
on this endpoint, unlike the hosted flow, so this is where you
have to act. Some national wallets accept nothing but an
encrypted response
(response_mode=direct_post.jwt) and, without one,
return no error at all: they read the request then give up, and
the session stays pending forever. If you target a
production wallet, turn it on.
Without encryption, redirect_uri is optional (it
serves the same-device flow); encryption makes it mandatory
(HAIP 1.0 §5.1). That address must be reachable from the device
running the wallet, not from your server: in a cross-device
flow, the wallet receives it after posting its response and
tries to open it on the phone. A loopback address
(http://localhost:...) points at the phone itself
there. The symptom is confusing: verification succeeded, the
result reaches you, and it is the phone that shows an error.
curl -X POST https://verify.todis.eu/verify/sessions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"vct": "https://example.eu/credentials/pid",
"claims": ["age_over_18"],
"country_code": "FR"
}'
201: session created, withsession_idandauthorization_request.400:claimsempty, or no valid trust anchor covers the requested type.401: missing, invalid, revoked, or expired license token.403: the token's plan does not include the full DCQL grammar (dcql_query, Premium plan).429: plan request quota exceeded.502: trust registry resolution failed (country_codemode).503: the trust registry the session depends on has no base loaded.
GET /verify/sessions/{id}
Retrieves the result of a session created by
POST /verify/sessions, to poll after the user has
responded with their wallet (or periodically while waiting).
curl https://verify.todis.eu/verify/sessions/<session_id> \
-H "Authorization: Bearer YOUR_TOKEN"
200: three possible statuses,{"status": "pending"},{"status": "verified", "claims": {...}}, or{"status": "failed", "error": "..."}. For an mdoc session (doctype/mdoc_claims),claimsnests the elements under their namespace. A verified session also carriestrust, where the anchor that validated it comes from.401: missing, invalid, revoked, or expired license token.404: unknown session (invalid identifier, or session expired after 15 minutes).429: plan request quota exceeded.
GET /trust/registries
Lists, with no token, the trust anchors this deployment
accepts: for each registry (mdoc,
sd_jwt_vc), its sources and anchors, with the
credential types each one covers and its certificate's validity
dates, and the countries a country_code can reach.
A source read from a VICAL, the signed list of mdoc issuing
authorities, says which VICAL it comes from, and
discarded lists the entries of that VICAL that did
not become anchors, with their reason: that is where a missing
expected anchor is explained. To report a missing anchor:
report_missing_anchor.
curl https://verify.todis.eu/trust/registries
200: registry anchors and the countries of the EU's official list, cacheable response (Cache-Control,ETag).304: unchanged since theETagsent inIf-None-Match.
GET /usage
Check your request volume directly with your license token, without waiting for a dashboard or contacting support: handy for checking your consumption before a Starter block runs out, or just tracking your activity over time. If you've repurchased several Starter blocks, the total automatically adds up every token associated with your email address, no need to tally them yourself.
curl https://verify.todis.eu/usage \
-H "Authorization: Bearer YOUR_TOKEN"
200:{"client_ids": [...], "total_authenticated_requests": 42}, ortotal_authenticated_requests: nullwith anerrorfield if metrics are temporarily unavailable (the rest of the response is still reliable).401: missing, invalid, revoked, or expired license token.429: plan request quota exceeded.
GET /usage/history
The daily history of your consumption, from the Standard plan
up: daily totals over 30 days. The Premium plan adds the
per-endpoint breakdown, latencies, a 400-day depth and CSV
export (?days=30&format=csv). No raw
verification data is retained, only aggregates.
200: JSON history, or CSV whenformat=csv.401: missing, invalid, revoked, or expired license token.403: the token's plan does not include history (Standard and above).429: plan request quota exceeded.501: deployment without a database (self-hosted withoutDATABASE_URL).
Response format (direct verification)
For /verify/sd-jwt-vc and /verify/mdoc:
a verified boolean and, depending on the outcome,
either the disclosed information (claims for
SD-JWT VC, doc_type/namespaces for
mdoc) or a text error field. Never the full
identity document, only the attributes actually disclosed (see
above).
Going further
For the overview (authentication, two ways to integrate, a
curl example), see
How the API works. For the exact
request/response schema,
todis.eu/en/docs
(Swagger). Have a question not covered here?
contact@todis.eu.