Portal / config

{BASE} is https://authentica-merge-integration.t2.sa (Integration API); you have client_id and secret
One URL is registered — your returnUrl, the GET landing page — matched as a whole string
The registered string and the returnUrl you send on /session are byte-identical: scheme, host, port, path, case, trailing slash, and no query string
The return URL does not sit behind a redirect (httphttps, trailing-slash canonicalisation, CDN host rewrite)
You send "channel": "sms" and a phone destination on /session

Backend

POST /session is called only from the server with Basic auth, and with an Idempotency-Key
A retried attempt reuses its key; a new attempt gets a new key (otherwise 422 idempotency_key_reused)
409 idempotency_request_in_flight is handled as “wait and retry with the same key”, not as a failure
userRef is stable for the same user across every action, not just login
Your backend sets operationSensitivity to "normal" or "sensitive" on /session (never trust the browser for this)
You store state, the returned sessionId, userRef, and the action they belong to before opening verify
state is opaque, random and single-use — no action name, no user id, no URL inside it
The landing route calls GET /session/{sessionId} with Basic auth and acts only on what it returns
It requires status == "verified", the echoed state to match your record, and userRef to match the user you started for
It ignores assertionExchanged — a legacy wire field with no meaning
Fulfilment is one idempotent onVerified(sessionId); a reload of the landing page performs the action once
A reconciliation sweep covers users whose browser never came back, inside the 24 h window
The secret is not in git, frontend env, or mobile apps
There is no receiver route, signature check, or dedupe store left over from an earlier integration

Frontend

Your start route returns both verifyUrl and handoff to the browser
The browser opens verify by auto-submitting a form: POST verifyUrl with hidden field hx (URL as returned — do not build it, do not append to it)
No code does window.location.href = verifyUrl, and handoff never appears in a URL
Verification is started when the user is ready — handoff is single-use and expires in 120 s
Nothing tries to iframe or embed the hosted page — it refuses to be framed
There is no callback route in the SPA, and no code reads secrets from the landing URL — session_id and state are identifiers only
The landing page contains no per-action branching — your backend decides what to show, keyed by state

Per action you protect

Run this once for each place you add verification (login, payment, phone change, approval…):
The start call sits behind your own checks for that action (permissions, balance, ownership)
operationSensitivity matches the real risk of that action
Cancelling or abandoning verification leaves the action unperformed

Smoke test

Session returns sessionId, verifyUrl and handoff; repeating with the same Idempotency-Key replays the response (Idempotent-Replayed: true) instead of sending a second SMS
Sending a returnUrl with a trailing slash added fails with invalid_return_url — proof the match is exact
Verification completes on the hosted page; the address bar never shows a query string on the verify host
The browser lands on returnUrl?session_id=…&state=… and the page shows the right outcome
Hand-editing session_id or state on the landing URL fulfils nothing
Reloading the landing page after success does not run the action twice
Completing a verification with the tab closed still resolves later via GET /session/{id}
Replaying the same handoff returns 400; so does GET on verifyUrl with any query string