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
httpvshttpswwwvs the bare domain- a localhost port that differs between your dev server and the registration
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:
- 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. - The
handoffwas already used. It is single-use, redeemed on the first POST — create a new session. - The
handoffexpired. TTL is 120 s, so call/sessionwhen the user is ready, not minutes ahead. - The body was not a form submission, or the
hxfield was missing.
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 callGET /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 itssessionId. 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 makeonVerified(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 offstate 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 instate (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 (http→https, 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/secretissued for your app — a session created by another client reads back aspending, 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 stableuserRef 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, responseerrorCode, the request path (without secrets), the sessionId, and the state you stored. Contact support with those details.
