invalid_return_url on session create

The returnUrl you sent is not one of the URLs registered for your app. The comparison is a whole-string match, so the usual culprits are cosmetic:
  • a trailing slash on one side and not the other
  • a query string appended to the value you send
  • http vs https
  • www vs the bare domain
  • a localhost port that differs between your dev server and the registration
Fix the registration or the value you send so they are byte-identical. This fails on POST /session, before the browser goes anywhere, so the user never sees it.

The verify page returns 400

The hosted page is opened by POSTing the handoff token to verifyUrl. A 400 means one of:
  1. You navigated to it, or appended something to it — GET {verifyUrl} with any query string is rejected outright. No flow value may travel in a URL on that path.
  2. The handoff was already used. It is single-use, redeemed on the first POST — create a new session.
  3. The handoff expired. TTL is 120 s, so call /session when the user is ready, not minutes ahead.
  4. The body was not a form submission, or the hx field was missing.
A common version of (2): the user pressed back to your start page and re-submitted the same stale form. Start a fresh session instead of re-rendering the old one. See Frontend overview.

The user reached my landing page but nothing happened

The landing page arriving is not a result. Nothing is delivered to your server — no callback, no webhook, nothing to wait for. If your landing route does not call GET /session/{sessionId}, it will never learn the outcome, however long it waits. The route must: resolve your pending record from state, call the status endpoint with Basic auth, and act on what it returns.

I trusted the query parameters

session_id and state on the landing URL are inputs to a lookup, not evidence. Anyone can type them. If your code does anything on the strength of their presence — signs a user in, captures a payment, marks a phone verified — that is the bug. Every fulfilment must be gated on a GET /session/{id} made with your client credentials, with status == "verified", the echoed state matching your record, and userRef matching the user you started for.

The user verified, closed the tab, and nothing happened

The redirect never fired, and there is no push channel to fall back on — so you have to ask. The outcome is readable for 24 hours under its sessionId. Run a periodic sweep over your unfinished pending records calling GET /session/{sessionId}, and feed verified results through the same idempotent onVerified.

The same action ran twice

The landing page can be hit more than once: a refresh, the back button, a user reopening the link from their history, or your reconciliation sweep arriving at the same time. All of that is expected. Mark the stored action used inside the same transaction that performs it, and make onVerified(sessionId) a no-op the second time.

409 idempotency_request_in_flight

You sent the same Idempotency-Key while the first request was still running — usually a double-click or an aggressive client retry. Wait briefly and retry with the same key; you will get the original response replayed with Idempotent-Replayed: true.

422 idempotency_key_reused

You sent the same key with a different payload. Almost always a key that is scoped too broadly — for example one key per user or per order instead of one per attempt. A retry of an attempt reuses its key; a new attempt (the user asked to send again, or changed the phone number) needs a new one.

Verification succeeds but the wrong action runs

Your completion logic is deciding what to do from something the browser sent. Key the action off state instead: store the action and its parameters server-side when you create the session, then look them up by the state the status read echoes back. Confirm userRef matches, then run that branch. See Routing many actions through one return URL.

My landing page turned into an open redirect

You put a destination URL in state (or in another query parameter) and redirected to it. Anyone can start a flow with state set to their own site and use your domain to bounce users there. Keep destinations server-side on the pending record, or map an action kind to a fixed set of routes in code. If the browser must hint at a destination, accept a short key and resolve it against an allow-list.

The landing page has no session_id or state

The return always carries ?session_id=…&state=…, so if the landing page sees neither, something between Authentica and your app dropped the query string — a redirect (httphttps, trailing-slash canonicalisation, a CDN host rewrite), or client-side routing that stripped it before you read it. Register the exact URL the browser lands on, keep it off any redirect, and read session_id/state before your router runs. If the values genuinely never arrive for some users, close those sessions out with the reconciliation sweep over GET /session/{sessionId}.

Frontend calls Authentica and fails CORS

Your frontend must call your start route. Your server then calls Authentica. Never put Basic auth in the browser.

Wrong host or credentials

  • Confirm AUTHENTICA_BASE_URL / {BASE} is the Integration API host: https://authentica-merge-integration.t2.sa
  • Register your return URL exactly in the portal
  • Use the client_id / secret issued for your app — a session created by another client reads back as pending, forever

invalid_channel on session create

Send "channel": "sms" with a phone destination. Any other channel value returns invalid_channel.

Passkey “Enable” fails

Use a stable userRef or enrolment will never stick across sessions. The hosted verify page is always a top-level page, where passkey creation works in the same tab.

Something in my code references a callback, webhook or assertion

Those are not part of this API — usually they come from an AI coding assistant guessing a generic OAuth/webhook shape, or from a sample written for a different provider. There is no callback, no webhook, no signature, no delivery log, no assertion and no /introspect. Delete that code: the outcome comes only from your authenticated GET /session/{id}. The one field that still says form_post is binding on the /session response, and it is correct: that describes how the hosted page is opened, not how anything comes back.

Still stuck

Collect: HTTP status, response errorCode, the request path (without secrets), the sessionId, and the state you stored. Contact support with those details.