| Landing page shows “pending” though the user verified | You never called GET /session/{sessionId}, or you called it with an id that is not yours | Read the status from the landing route with Basic auth, using the sessionId from your own pending record |
status: "pending" for an id you believe exists | pending also covers “unknown id” and “belongs to another client” — there is no 404 | Trust your own record for existence; check you are using the right client_id |
| The action ran for the wrong user | You acted on the status without comparing userRef to your record | Require state and userRef to match before fulfilling |
| The action ran even though the user never verified | You treated arrival on the landing URL as success. A failed verification returns the browser with the same ?session_id=&state= a successful one does | Gate fulfilment on status == "verified" from GET /session/{id}, never on arrival |
status: "failed" and you want to know why | reason says which terminal condition ended it: otp_locked, device_changed, session_binding_failed | Show it to the user and let them start a new session. Branch on status; treat an unknown reason as generic failure |
| The action ran twice | The landing page was reloaded, or your sweep also fired | Make onVerified(sessionId) idempotent: perform and mark used in one transaction |
Landing page arrived without session_id/state | The return always carries them, so something between Authentica and your app rewrote the URL (a redirect that dropped the query string, a router that stripped it) | Register the exact landing URL, and read session_id/state before any client-side routing runs |
| Your landing route is an open redirect | You stored a URL in state and redirected to it | Never do that. Keep destinations server-side — see Routing many actions |