Portal / config
{BASE} is https://authentica-merge-integration.t2.sa (Integration API); you have client_id and secretOne URL is registered — your
returnUrl, the GET landing page — matched as a whole stringThe registered string and the
returnUrl you send on /session are byte-identical: scheme, host, port, path, case, trailing slash, and no query stringThe return URL does not sit behind a redirect (
http→https, trailing-slash canonicalisation, CDN host rewrite)You send
"channel": "sms" and a phone destination on /sessionBackend
POST /session is called only from the server with Basic auth, and with an Idempotency-KeyA 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 failureuserRef is stable for the same user across every action, not just loginYour 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 verifystate is opaque, random and single-use — no action name, no user id, no URL inside itThe landing route calls
GET /session/{sessionId} with Basic auth and acts only on what it returnsIt requires
status == "verified", the echoed state to match your record, and userRef to match the user you started forIt ignores
assertionExchanged — a legacy wire field with no meaningFulfilment is one idempotent
onVerified(sessionId); a reload of the landing page performs the action onceA 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 browserThe 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 URLVerification is started when the user is ready —
handoff is single-use and expires in 120 sNothing 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 onlyThe landing page contains no per-action branching — your backend decides what to show, keyed by
statePer 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 actionCancelling 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 SMSSending a
returnUrl with a trailing slash added fails with invalid_return_url — proof the match is exactVerification 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 outcomeHand-editing
session_id or state on the landing URL fulfils nothingReloading 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
