Your frontend never talks to Authentica with secrets. It only:
  1. Calls your backend to start verify
  2. Opens the hosted verify UI by POSTing the handoff token to verifyUrl (a hidden auto-submitting form)
There is no third step for the frontend. The browser is redirected back to your registered returnUrl afterwards, and your server works out what happened by calling GET /session/{sessionId}. Backend setup first: Get your credentials · Backend overview. End-to-end: Quickstart. Building a native mobile app? See Mobile apps — the same backend calls, with the return delivered to a deep link your OS routes into your app.

The browser half

The whole browser half is only a few lines: an auto-submitting form on the way out, and a plain landing page on the way back. No dependency, no build step — shown below and on each framework page. Either way your backend still makes the two authenticated calls.

Handoff — auto-submitting form

Server-rendered equivalent, if your start route responds with HTML:
window.location.href = verifyUrl does not work. verifyUrl is a bare endpoint with no query string, and GET {verifyUrl}?… returns 400. The handoff token is single-use with a 120 s TTL and must never appear in a URL.
Optional extra hidden field: lang, to force the UI language. The hosted page is always a full top-level page. It sends frame-ancestors 'none', so it cannot be embedded in an iframe, and there is no inline or embedded mode and no postMessage API — the form submit above is the only way in.

The landing page — a plain GET, nothing sensitive

After verification, Authentica redirects the browser to your registered returnUrl with an ordinary top-level GET:
Neither value is a credential — both are useless without your client secret, which never leaves your server. Because it is a plain GET to your own origin:
  • your normal SameSite=Lax cookies arrive as usual — no cookie tricks
  • no CSRF exemption is needed, and there is no POST route to build
  • it can be served by any stack, like any other page of your app
The server side of that route is what finishes the verification: it calls GET /session/{session_id} with your client credentials, checks the echoed state and userRef, and performs the action. The browser’s only job is to display whatever your backend concludes. Never render “verified” just because the redirect arrived. There is no callback route in your SPA, and none on your server either — nothing is ever pushed to you. There is nothing to listen for.

One landing page for every action

Whatever you are verifying — a login, a payment, a phone change — the browser side is identical: start, POST into verify, land back. You register one returnUrl for the app, and your backend uses state to recognise which action the user was in the middle of and what to show next. See Routing many actions through one return URL. That means the browser needs no per-action logic. Do not branch on query params or store the action in localStorage; your backend owns that decision, keyed by state.

Rules

  • Do not put client_id or secret in the browser
  • Do not call Authentica /session or /session/{id} from frontend JS
  • Do not navigate to verifyUrl — POST to it
  • Do not append handoff, lang, or anything else to verifyUrl
  • Do not try to embed the hosted page — it refuses to be framed
  • Do not treat session_id or state on the landing URL as proof of anything — ask your backend
  • Register returnUrl exactly in the portal

Pick your framework

JavaScript

Plain fetch + auto-submitting form

React

Form POST from a React handler

Vite

Vite + React form-POST pattern

Angular

Component start + form POST