returnUrl for the app. Every verification in your product — login, payment, phone change, an admin approval — comes back to the same route. state is what tells that route which one it is.
The pattern
1
Mint an opaque, single-use `state`
A fresh random value per attempt (a UUID or 128+ bits of randomness). It carries no meaning of its own: it is a lookup key, not data.
2
Write a server-side pending record before you hand off
Keyed by
state, holding at least:sessionId comes from the POST /session response, so store the record — or update it — as soon as that call returns. Give it a short TTL; a pending record that outlives the session is just clutter.3
On the landing route, resolve `state` → record
Look the record up. No record, or one already marked
done? Render accordingly and stop — do not invent a flow from the URL.4
Read the outcome and cross-check
Call
GET /session/{sessionId} with Basic auth, using the sessionId from your record. Require that the echoed state matches, and that userRef matches the user on the record. Only then branch on kind and run the action.5
Mark the record used in the same transaction
The landing page can be hit twice. Fulfil idempotently: perform the action and set
done atomically.Rules that keep it safe
stateis opaque. No action name, no user id, no amount, no JSON, no signed blob. Anything readable instateis readable by the user, and anything meaningful in it is something they can change.- Single use. One
statebelongs to one attempt. A retry gets a new one. - Bind
userRefto the record. Store the user you started for, and compare it to theuserRefthe status endpoint returns. This is what stops a session started for one user being used to complete an action for another. - Never trust the query string.
session_idandstateon the URL are inputs to a lookup, never evidence. The action runs on whatGET /session/{sessionId}says. - Do not branch in the browser. The landing page needs no per-action logic and no
localStorage; your server owns the decision, keyed bystate.
Why not one return URL per action?
You can register more than one URL on the app, but you rarely want to: every extra URL is another exact-match string to keep in sync between your code and the portal, and it puts the action type on the front channel where you then have to distrust it anyway. One URL plus a server-side record is fewer moving parts and strictly less to validate.Related
- How it works — where this fits in the flow
- Session status — the read that makes it trustworthy
- Backend overview — per-language landing routes

