> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Redirect URLs

> Where Portal returns a user after a sign-in, how the allow list matches, and how your app receives the redirect.

Every Portal-managed sign-in ends the same way: Portal redirects the user to a
URL you control, with a single-use token appended. That URL is your
**redirect URL**, and it has to be allow-listed for the auth environment before
any sign-in will start.

This page is the source of truth for how redirect URLs are matched and what
Portal appends to them. For the SDK calls that consume the redirect, see the
[Web](/sdks/web/guide/client-auth) Client Auth guide.

## Two different redirects

Two URLs in this flow are both called a redirect, and they are not the same
thing.

|                           | Where it points                                                                       | Where you register it                                    |
| ------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| **Provider redirect URI** | Portal, at `https://api.portalhq.io/api/v3/auth/oauth/callback/<AUTH_ENVIRONMENT_ID>` | Google Cloud Console or your Apple Services ID           |
| **Redirect URL**          | Your app                                                                              | The **Redirect URLs** allow list in the Portal dashboard |

The provider redirect URI is how Google or Apple hands the user back to Portal,
and it is covered in [Google OAuth](/resources/authentication/google-oauth) and
[Apple OAuth](/resources/authentication/apple-oauth). Everything below is about
the second one.

## Supported forms

| Form                       | Example                             | Use it for                             |
| -------------------------- | ----------------------------------- | -------------------------------------- |
| HTTPS URL                  | `https://example.com/auth/callback` | Web apps                               |
| Custom scheme              | `myapp://auth/callback`             | Mobile apps                            |
| App Link or Universal Link | `https://example.com/auth/callback` | Mobile apps that own a verified domain |

An App Link or Universal Link is not a separate kind of allow-list entry. To
Portal it is an ordinary HTTPS URL; what routes it to your app instead of a
browser is the association file you host on that domain
(`assetlinks.json` on Android, `apple-app-site-association` on iOS). Portal
appends its parameters the same way either way.

<Note>
  Custom schemes are valid allow-list entries. If you are integrating a mobile
  app, register the same custom scheme URL you will pass to the SDK — you do not
  need to host a web page to complete a sign-in.
</Note>

## How matching works

Portal compares your redirect URL against the allow list by **exact string
comparison**. There are no wildcards and no path prefixes.

| This is on your allow list     | This will be rejected                                   |
| ------------------------------ | ------------------------------------------------------- |
| `https://example.com/callback` | `https://example.com/callback/` — trailing slash        |
| `https://example.com/callback` | `https://example.com/callback?next=/home` — extra query |
| `myapp://auth/callback`        | `MyApp://auth/callback` — different case                |

A request whose `redirectUrl` is not on the list is rejected with a `401`. This
is what stops someone who has your Auth Environment ID from sending your users,
and their sign-in tokens, to a site they control. On the OAuth path the check
runs twice: once when the sign-in starts and again when the provider calls back.

<Warning>
  The Portal SDKs compare redirect URLs **more loosely than the backend does**.
  Locally they lowercase the scheme and authority, strip trailing slashes, and
  ignore the query string and fragment, so an SDK can accept a URL that the API
  then refuses with a `401`. Never treat a successful local match as proof the
  value is allow-listed — use the exact string in every place below.
</Warning>

## Use one string everywhere

The same redirect URL has to appear, byte for byte, in up to three places.

1. **The dashboard.** The **Redirect URLs** allow list under
   **Authentication > Configure**. See
   [Enable authentication](/resources/authentication/enable-authentication).
2. **Your SDK configuration.** `redirectUrl` in the Web SDK's `clientAuth`
   option, or the equivalent for whichever client you are using.
3. **Your native app registration.** On mobile only: the URL scheme in
   `Info.plist` and the intent filter in `AndroidManifest.xml`.

Each mismatch fails differently, which is worth knowing when you are debugging:

* **Dashboard and SDK disagree** — the sign-in never starts. You get a `401`
  from the call that begins the flow.
* **SDK and native registration disagree** — the sign-in completes, but the
  redirect never reaches your app. Nothing throws; the user is left in the
  browser.

## What Portal appends

Portal appends its parameters to your redirect URL, using `?` or `&` depending
on whether your URL already has a query string.

| Outcome                           | Appended parameters                                     |
| --------------------------------- | ------------------------------------------------------- |
| Google or Apple sign-in completed | `token=<GRANT>&login_type=GOOGLE` or `login_type=APPLE` |
| Magic link opened                 | `token=<GRANT>&authMethod=EMAIL_MAGIC_LINK`             |
| OAuth sign-in failed              | `error=oauth_failed`                                    |

```
https://example.com/callback?token=eyJhbGciOi...&login_type=GOOGLE
https://example.com/callback?token=eyJhbGciOi...&authMethod=EMAIL_MAGIC_LINK
https://example.com/callback?error=oauth_failed
```

If your redirect URL already carries a parameter named `token`, the SDKs read
the **last** occurrence, which is always the one Portal appended.

## Grants are single use

The `token` parameter is a **grant**: proof that a sign-in completed. It is
short-lived and single use, and it is spent the moment the backend sees it.

| Grant                 | Lifetime   |
| --------------------- | ---------- |
| Magic link            | 15 minutes |
| OAuth (`state` nonce) | 10 minutes |

Once a grant has been exchanged or has expired, the user has to sign in again.
There is no way to refresh one.

## When a sign-in fails

The two methods fail differently, and your app has to handle both.

**OAuth** returns the user to your redirect URL with `error=oauth_failed` and no
token. The value is deliberately generic — it covers a cancelled consent
screen, a disabled method, and a rejected state nonce alike. Treat it as "the
user did not complete the sign-in" and offer to start again.

**Magic links have no error redirect.** An expired or already-used link still
arrives at your redirect URL carrying a `token`, and the failure only surfaces
when your app exchanges it. So a redirect that looks valid can still fail a
moment later. Your app needs an error path on the exchange, not just on the URL.

## A redirect can arrive twice

Operating systems re-deliver deep links, and a browser can reload a callback
page. Because a grant is spent on first use, a second delivery of the same
redirect cannot produce a second session.

Route every redirect through **exactly one** handler in your app. If two code
paths both consume the same URL, the second one fails on an already-spent
grant, which looks like a genuine authentication error to your user.

## Register the redirect in your app

<Tabs>
  <Tab title="Web">
    Add a route at the exact URL on your allow list. It has to be reachable by
    a full-page navigation, because the provider redirects the browser to it.

    ```
    https://example.com/auth/callback
    ```

    The page reads its own URL and hands it to the SDK. See
    [Client Auth](/sdks/web/guide/client-auth) for the callback route.
  </Tab>
</Tabs>

<Note>
  Client Auth is available in the Portal Web SDK. If you are integrating on
  another platform, you can drive the same flow against the
  [Authentication API](/resources/authentication/overview) directly.
</Note>

## Next steps

<Card title="Web Client Auth" icon="globe" href="/sdks/web/guide/client-auth">
  Build a callback route and complete a sign-in in a web app.
</Card>
