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

> ## Agent Instructions
> Unkey is two separate products. Compute builds, deploys, and runs apps behind a gateway. API Management issues API keys, enforces rate limits, manages identities and permissions, and reports usage. Say which product a page belongs to; a reader can use either without the other.
> Every Unkey API endpoint is an HTTP POST to https://api.unkey.com/v2/{service}.{procedure} with a root key in the Authorization: Bearer header. Root keys are workspace scoped.
> Error codes have the form err:{system}:{category}:{specific} and each has a page at /errors/{system}/{category}/{specific}.
> The word environment means production or preview in Compute. Rate limiting has four meanings on this site; the glossary lists them.

# Portal sessions

> Sign a user into the portal from your backend.

Sign a user into the [developer portal](/docs/api-management/portal/overview) from your backend, with no second login. After they sign in to your app, ask Unkey for a session for that user and redirect their browser to the URL you get back. Your root key never reaches the browser.

## Sign a user in

<Steps titleSize="h3">
  <Step title="Your backend mints a session">
    Call `portal.createSession` with the portal, the user's `externalId`, and the scopes they should have. You get `{ "id": "ps_...", "url": "https://portal.unkey.com/?code=pec_..." }`. The URL works **once**, within **15 minutes**.
  </Step>

  <Step title="Redirect the user">
    Send the browser to `url`.
  </Step>

  <Step title="The portal exchanges the code">
    The portal swaps the code for an access token that lasts **24 hours**. If the code is expired, already used, or unknown, the user sees `err:unkey:authentication:portal_session_not_found`, "Session is invalid, expired, or has already been used."
  </Step>

  <Step title="The user works, then leaves">
    The user only sees their own keys. When the token expires, or they click the return link, the portal sends them to the `returnUrl` you set. Without one, there's no return link. To let them back in, create a new session.
  </Step>
</Steps>

You only call `portal.exchangeCode` yourself if you build your own portal front end. It returns `{ "accessToken": "pat_...", "expiresAt": <ms> }`.

## Request

<Note>
  You need a root key with the permissions listed on this page. Create one in the dashboard under **Settings > Root Keys**, and pass it as `Authorization: Bearer <root key>`. See [Permission reference](/docs/platform/root-keys/permissions-legacy) for every permission.
</Note>

<ParamField body="portal" type="string" required>
  The portal's `pc_` ID or slug.
</ParamField>

<ParamField body="externalId" type="string" required>
  The user's ID in your system, 1 to 256 characters. A session for `user_123` shows the keys whose identity is `user_123`.
</ParamField>

<ParamField body="scopes" type="string[]" required>
  One or more of `keys:read`, `keys:reroll`, and `analytics:read`. Scopes decide what the user can see and do. See [What portal users can do](/docs/api-management/portal/end-user-experience). `keys:reroll` and `analytics:read` each need `keys:read` too. Without it, the request fails with 400 and `err:unkey:application:invalid_input`.
</ParamField>

<ParamField body="returnUrl" type="string">
  Full URL, up to 500 characters, to send the user back to when they leave or the session expires. You set it per session, so different users can go back to different pages.
</ParamField>

<ParamField body="preview" type="boolean" default="false">
  Create a preview session to try the portal yourself.
</ParamField>

## Permissions your root key needs

Your root key needs `portal.*.create_portal_session` or `portal.<portalId>.create_portal_session`. Without it, the request fails with **404**, not 403.

It also needs a matching permission for each scope, on every keyspace the portal shows, either as `*` or for each `api_` ID:

| Scope | Required on the root key |
| - | - |
| `keys:read` | `read_key` and `read_api` |
| `keys:reroll` | `create_key`, plus `encrypt_key` if the keyspace stores encrypted keys |
| `analytics:read` | `read_analytics` |

If the root key is missing one, the whole request fails with **403**. A disabled portal can't create sessions at all.

## What ends a session

A session ends when its 24 hour token expires, or when the portal is deleted or pointed at a different keyspace or app. That usually takes effect within about 10 seconds, but can take up to 5 minutes. Disabling a portal doesn't end live sessions. You can't end a single session, so choose scopes with the 24 hour lifetime in mind.

Creating and exchanging a session write `portal.session.create` and `portal.session.exchange` [audit log](/docs/api-management/audit-logs/event-types) entries. What the user does in the portal is logged under a `portalEndUser` actor.

## Errors

| Status | Code | Meaning |
| - | - | - |
| 404 | `err:unkey:data:portal_not_found` | The portal does not exist, is not in your workspace, or your root key lacks `create_portal_session` for it. |
| 403 | `err:unkey:authorization:insufficient_permissions` | A requested scope needs a keyspace permission the root key lacks. |
| 401 | `err:unkey:authentication:portal_session_not_found` | On exchange: the code is expired, used, or unknown. On end-user endpoints: the token is expired or revoked. |
| 400 | `err:unkey:application:invalid_input` | `keys:reroll` or `analytics:read` was requested without `keys:read`. |
| 400 | `err:unkey:authentication:portal_token_missing` | An end-user endpoint was called without a portal token. |
