Your backend makes two Authentica callsPOST /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)

Four values. 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

Response:
Store 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:
If 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

Field details: Create session · Session status · API overview.

Rules

The frontend never sees Basic auth credentials. Only your backend talks to Authentica.
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.
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.
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.
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.
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.
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).
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.
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