POST /session to start, GET /session/{id} to read the outcome — and receives nothing. There is no callback, no webhook, no signature, no delivery to acknowledge. Everything else is your own app logic.
Collect {BASE}, client_id, secret and your one registered return URL first: Get your credentials. Happy-path wiring: Quickstart.
The browser opens verifyUrl (use it as returned) by POSTing the handoff token to it. When the user finishes, the browser lands on your returnUrl with a plain GET carrying ?session_id=…&state=… — and your landing route asks GET /session/{sessionId} what actually happened.
Routes you add
Two routes. You can rename them; only the Authentica URLs are fixed. Both back-channel calls use
{BASE} = https://authentica-merge-integration.t2.sa.
These routes are not login-specific. Put the start call inside whichever handler owns the action — POST /auth/login, POST /payments/confirm, POST /profile/change-phone, POST /admin/approve — or keep one generic pair and pass your own action type into it. Store what the action is against state and read it back on completion. See Routing many actions through one return URL.
Required configuration (server env)
AUTHENTICA_RETURN_URL must be registered in the portal and match what you send on /session as a whole string (else invalid_return_url).
Authentica calls (curl)
-u is HTTP Basic (client_id:secret). Run these from your server only — never from the browser.
Create session
result.sessionId next to state, then return both result.verifyUrl and result.handoff to the browser, which POSTs handoff to verifyUrl as the hx form field. Never put handoff in a URL.
Session status
From your landing route, and from a reconciliation sweep over pending records:result.status is "verified", check result.state matches your record and result.userRef is the user you started for, then run your idempotent onVerified(sessionId).
Revoke
Rules
Keep the secret on the server
Keep the secret on the server
The frontend never sees Basic auth credentials. Only your backend talks to Authentica.
Store `state`, `sessionId` and `userRef` before opening verify
Store `state`, `sessionId` and `userRef` before opening verify
Generate an opaque
state, store the action and its parameters against it (the payment id, the new phone number, the record id), send it on /session, and save the returned sessionId and the userRef on the same record. state is how the landing page finds the action; sessionId is how you read the outcome. The record must never come from the browser.Trust only an authenticated status read
Trust only an authenticated status read
Never perform the action because the browser arrived on
returnUrl — those query values are unauthenticated inputs to a lookup, not evidence. Act on GET /session/{id} made with your credentials, and match state to your pending action and userRef to the user it belongs to.Make fulfilment idempotent
Make fulfilment idempotent
The landing page can be reloaded, and your reconciliation sweep can reach the same session. One
onVerified(sessionId), safe to run twice: mark the pending record used in the same transaction that performs the action.Use a stable `userRef`
Use a stable `userRef`
Same person → same
userRef every time (your user id / GUID), across every action you verify. Do not use a new random id per request unless you intend a new identity.Send an Idempotency-Key on /session
Send an Idempotency-Key on /session
It is the call your HTTP client retries on timeout and the one that costs money in SMS. A replayed key returns the original response instead of texting the user twice. See Idempotency.
Set `operationSensitivity` on your backend
Set `operationSensitivity` on your backend
Your server sets
"normal" or "sensitive" when calling /session. Do not accept this from the browser — the frontend only starts the action; your backend decides how risky it is. Use "sensitive" for money movement, permission changes, and irreversible operations (it forces OTP even when a passkey exists).Cover the closed tab
Cover the closed tab
If the user verifies and never comes back, no redirect fires. A periodic sweep over your pending records, one
GET /session/{id} each, closes those out inside the 24 h retention window.SMS only
SMS only
Send
"channel": "sms" and the phone as destination. Your server sets the channel — do not take it from the browser.Pick your language
C# / ASP.NET
HttpClient + controllers
Node.js
Express
Java
Spring Boot
PHP / Laravel
Laravel HTTP client

