The hosted flow step by step
Technical guide: create a verification journey, redirect the user to the page hosted by Todis, confirm the result server-side, and receive signed webhooks. The other guides: online shops and mobile apps and native integration.
Create, redirect, confirm
Create the journey (here, an age check):
curl -X POST https://verify.todis.eu/hosted/sessions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"check": "age_over_18",
"locale": "en",
"success_url": "https://your-shop.example/verification-return",
"reference": "order-1042"
}'
Response: the URL to redirect to, and the identifier to keep.
{
"session_id": "…",
"hosted_url": "https://verify.todis.eu/v/…",
"expires_at": "2026-08-19T12:34:56Z"
}
Redirect your user's browser to hosted_url: Todis
displays the QR code (wallet on another device), the open button
(wallet on the same device), and handles the wait in the requested
language. The button carries a same-device request, whose result
is only released in the browser that opened the page first: so do
not open hosted_url in a browser before redirecting
the user to it. purpose (optional) displays the
purpose of the verification on the page, in your words, and
cross_device: false removes the QR code (see the
technical reference).
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. When the user returns to your success_url,
your backend confirms (never on the sole basis of URL parameters,
which the user can forge):
curl https://verify.todis.eu/verify/sessions/SESSION_ID \
-H "Authorization: Bearer YOUR_TOKEN"
{ "status": "verified", "claims": { "age_over_18": true } }
The "check": "identity" shortcut requests family
name, given name, birth date and nationality instead (the basis of
a KYC onboarding), and every advanced field of the
session API remains
available. cancel_url (optional) receives the user on
failure. The page carries Todis branding; a white-label display is
under consideration for the Premium plan.
The webhook, your safety belt
If the user closes the page before the redirect, you still get
told: add webhook_url at creation and your backend
receives a JSON POST
{"event": "session.verified", "session_id": "…", "reference": "…"}
when the journey concludes (automatic retries as long as you do
not answer 2xx). Every delivery is signed HMAC-SHA256 in the
X-Todis-Signature header with the
webhook_secret returned at creation:
// Node.js: verify the t=<timestamp>,v1=<hex> signature
const crypto = require("node:crypto");
function validSignature(body, header, secret) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret)
.update(t + "." + body).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
The webhook never contains the verified data: it only tells you
the journey is over, and you read the result through the
authenticated call above. For a wallet on the same device,
session.verified is sent when the hosted page
releases the result, on the wallet's return, and not as soon as
the wallet posts its response. The event says of the session what
the authenticated call says, minus the verified data:
trust, revocation, checks and
transport on a session.verified,
code and, when a presentation was judged,
checks on a session.failed.
The QR in your own page: the web component
If you would rather keep the user on your page than redirect them, a web component embeds the hosted page and reports state changes to you:
<script src="https://todis.eu/js/todis-verify.js"></script>
<todis-verify hosted-url="HOSTED_URL_GENERATED_BY_YOUR_BACKEND"></todis-verify>
<script>
document.querySelector("todis-verify")
.addEventListener("todis:verified", () => {
// Unlock the next step of YOUR interface,
// then confirm server-side as always.
});
</script>
Emitted events: todis:status on every change, then
todis:verified, todis:failed or
todis:expired. The hosted_url is always
generated by your backend: your license token never leaves your
server. Inside the component, the button that opens the wallet
opens the hosted page in a new tab; the QR code stays in your
page.
The three rules that never change. The license
token stays server-side. The result is confirmed through an
authenticated GET /verify/sessions/{id}, never on the
basis of a URL parameter or a browser event. And there is nothing
to store: no ID copy, no photo, only the verified answer you asked
for, as explained in the
FAQ.