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",
    "country_code": "FR",
    "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. 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.

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.

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.