Same form-POST pattern as React. Call your backend only. Use verifyUrl as returned — do not build it, do not append to it, and do not navigate to it. Proxy /verify/* to your API in vite.config if needed so fetch("/verify/start") stays same-origin.
Your registered returnUrl is a plain GET landing page, so in dev it can be either a route proxied to your API or a served SPA route — the redirect is an ordinary navigation. Whatever you register must be the exact string your server sends on POST /session, dev port included.

Start verify

The landing page

The browser comes back to returnUrl with ?session_id=…&state=… — two non-secret identifiers. The verification itself is finished on your server, which reads GET /session/{id} with its own credentials. The landing component asks your backend for the outcome and renders; see the React example. There is no callback route in the SPA and nothing sensitive to read from the URL. More: Frontend overview · React · Backend overview