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.