> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# How to Authenticate Requests to the Rolter Gateway

> Pass a virtual key in every request using the OpenAI or Anthropic header style; manage key access and expiry through virtual key properties.

Every request that reaches Rolter must carry a virtual key — a gateway-scoped credential that controls which models a client can access and counts against any configured budgets or rate limits. It is not the key your provider issued, and not the admin token: see [Which key do I need?](/security/which-key) if you are not sure which one you are holding. Rolter accepts the same virtual keys in two header styles so it stays compatible with any OpenAI- or Anthropic-SDK-based client out of the box.

## Header styles

Rolter recognises two authentication headers. Use whichever your SDK or client already produces — both accept the same virtual keys.

<CardGroup cols={2}>
  <Card title="OpenAI style" icon="key">
    `Authorization: Bearer <virtual-key>`

    Used with `/v1/chat/completions`, `/v1/completions`, and `/v1/models`.

  </Card>

  <Card title="Anthropic style" icon="key">
    `x-api-key: <virtual-key>`

    Used with `/v1/messages`.

  </Card>
</CardGroup>

<CodeGroup>

```bash OpenAI style
curl http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-rolter-dev" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'
```

```bash Anthropic style
curl http://localhost:4000/v1/messages \
  -H "x-api-key: sk-rolter-dev" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude","max_tokens":256,"messages":[{"role":"user","content":"hi"}]}'
```

</CodeGroup>

## Open mode (local development)

When no `[[virtual_keys]]` entries are present in your configuration, Rolter runs in **open mode** and forwards every request without checking credentials. This is convenient when developing locally against a private gateway instance.

```toml rolter.toml
# Remove or comment out all [[virtual_keys]] blocks to enable open mode.
# [[virtual_keys]]
# key = "sk-rolter-dev"
```

<Warning>
  Never run in open mode on a publicly reachable gateway. Any caller would be able to consume your upstream provider quota without restriction.
</Warning>

## Configuring virtual keys

Add one `[[virtual_keys]]` block per key to your `rolter.toml`. Each block supports the following properties:

<ParamField path="key" type="string" required>
  The raw key value clients present in the `Authorization` or `x-api-key` header.
</ParamField>

<ParamField path="name" type="string">
  A human-readable label for the key — shown in logs and the dashboard.
</ParamField>

<ParamField path="models" type="array of strings">
  An allow-list of model names this key may request. An empty array (the default) grants access to every route.
</ParamField>

<ParamField path="disabled" default="false" type="boolean">
  Set to `true` to revoke the key immediately without removing it from your config. The key will be rejected as if it did not exist.
</ParamField>

<ParamField path="expires_at" type="string (RFC 3339)">
  An optional expiry instant in RFC 3339 format. Rolter rejects the key at or after this time. Leave unset for a non-expiring key.
</ParamField>

```toml rolter.toml
[[virtual_keys]]
key = "sk-rolter-dev"
name = "local-dev"
models = []           # empty = all models allowed
disabled = false

[[virtual_keys]]
key = "sk-rolter-prod-readonly"
name = "read-only client"
models = ["gpt-4o"]
expires_at = "2027-01-01T00:00:00Z"
```

<Note>
  In the dashboard the same allow-list is a list of the project's routes to tick
  off, so a model name never has to be typed from memory. A model the project
  does not route yet can still be added by hand, and an entry already on a key
  that no route serves is kept rather than dropped — the stored value is the
  same array of names either way.
</Note>

<Warning>
  `sk-rolter-dev` is published in the repository, the README and these docs, and it allows every model. A gateway that serves other hosts must not carry it. When it is an active virtual key and the gateway listens on anything but a loopback address, the gateway logs a warning at startup and again on every config reload that still has it, and `rolter check` reports it as a failure. Delete its `[[virtual_keys]]` entry and mint a key of your own.
</Warning>

<Note>
  The raw key value is visible only at the moment you add it to your configuration or create it via the API. Store it in a secrets manager (such as a CI secret or a vault) immediately — it cannot be recovered later.
</Note>

## Session affinity

For multi-turn conversations where you need the same upstream replica to handle every turn, attach a session identifier to your request. Rolter uses this header with the `consistent_hash` and `pipeline` load-balancing strategies to pin the conversation to one backend.

```bash
curl http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-rolter-dev" \
  -H "x-session-id: user-42-thread-7" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"continue our chat"}]}'
```

<Tip>
  Use a stable, opaque identifier like a user-and-thread composite key. Rolter hashes the value to select a target — it is never forwarded to the upstream provider.
</Tip>

## Deployment-wide ingress policy

**Settings → Security** carries two rules that every gateway in the fleet
picks up on its next config poll — no restart, no redeploy.

<Note>
  There is no **Enforce virtual keys on inference** switch on this screen any
  more. A gateway that holds virtual keys always requires one, outside the
  auth bypass routes below. A gateway that
  holds none decides from how it was started: one managed by a control plane
  (started with `--snapshot-url`) refuses every request with `401`, and one
  running from its own config file stays open for local development. Only a
  `require_auth` line in that gateway's own config file changes this, in either
  direction — `require_auth = false` is the one way to open a managed gateway
  that holds no keys, and it belongs to whoever runs the process rather than
  to the dashboard.

The switch only ever reached managed gateways, which were already closed, so
it changed nothing and was removed. Saving from an older dashboard that still
sends the field is accepted; the field is ignored. A config file that still
sets `[security] virtual_key_required = true` is read as
`[server] require_auth = true`, with a warning to move it.

</Note>

<AccordionGroup>
  <Accordion title="Required headers">
    Name/value pairs every request must carry — a service-mesh identity header,
    a WAF marker, a tenant tag. A request missing one, or carrying a different
    value, is refused with `403 required_header_missing` before routing and
    before authentication.

    The refusal names the header but never the expected value, so a shared
    secret can safely be one. `/healthz` and `/readyz` are exempt: a probe is
    your own deployment's traffic, and filtering it takes the gateway down
    rather than protecting it.

    A saved rule that the control plane cannot read, which only a row written
    around the API with SQL can cause, is never read as "no rule". The control
    plane stops publishing config instead, so every gateway keeps enforcing the
    rule it has, a gateway that restarts in the meantime serves nothing until it
    is repaired, and parts of the dashboard that show the effective config
    return an error. `GET /api/v1/config/problems` names the setting and the
    entry at fault, and the **Effective config** screen lists that line under
    its error. Saving the rule again on this screen, or with
    `PUT /api/v1/security-settings`, repairs it. See
    [Upgrading](/deployment/upgrading#security-and-egress-settings).

  </Accordion>

  <Accordion title="Auth bypass routes">
    Exact `/v1/...` paths served without a key — typically `/v1/models` for a
    sidecar that reads the catalogue. Matching is exact: listing `/v1/models`
    does not open `/v1/models/gpt-4o`, and wildcards are rejected when you save.

    Nothing is attributed to a key on a bypassed path, because no key was
    presented. Budgets and per-key rate limits do not apply there.

  </Accordion>
</AccordionGroup>

### Editing the lists

Allowed origins, allowed headers, required headers and auth bypass routes are edited one entry per line. A comma is not a separator, so a required header value can contain commas (`x-ids: 1,2,3`).

The dashboard checks every line against the rules the control plane applies when you save. An entry that fails stays in the field with its line number named under it, Save stays off until every entry passes, and nothing is sent in the meantime. An entry is never dropped to make a save go through.

| List               | Each line must be                                                                               |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| Allowed origins    | an exact `http://` or `https://` origin, with no wildcard, path, query, fragment or user info   |
| Allowed headers    | a header name: letters, digits and the punctuation RFC 9110 allows in a token, no spaces        |
| Required headers   | `name: value`, split at the first colon, with a valid header name and a value that is not empty |
| Auth bypass routes | an exact path under `/v1/`, with no wildcard, placeholder, query, fragment or spaces            |

One rule is the dashboard's own. The control plane stores required headers as a map, so a header named on two lines would keep only one of the two values. The dashboard refuses the second line instead of choosing for you, and compares names without regard to case, as the gateway does.

### Saving a change that loosens security

A save that adds an **Auth bypass route**, or removes a **Required header**, asks first. The dialog lists exactly what was added or removed, says what each one means, and nothing is sent until you confirm. Cancelling keeps your edits in the form. A save that only tightens, such as a route removed or a header required, goes out at once, and so does adding an **Allowed origin**.

Save is enabled only while something differs from what was loaded. The cards you edited are marked **Edited**, and **Discard changes** puts every field back.

### After you save

The footer states when the save landed and which gateways run it. The control plane compares the config version each gateway last reported with the current one, the same `converged` flag the Cluster screen shows, and the footer counts the live gateways that match. A gateway applies a save on its next config poll, every 5 seconds by default and at once when Redis is configured. A gateway that has never identified itself to the control plane cannot be counted, and the footer says that instead of claiming it picked the change up.

## Error responses

Rolter returns standard HTTP error codes for authentication and authorisation failures.

<AccordionGroup>
  <Accordion title="401 — Missing or invalid key">
    Returned when the request carries no auth header, or when the presented key does not match any active virtual key. Check that the `Authorization: Bearer` or `x-api-key` header is present and contains the correct value.
  </Accordion>

  <Accordion title="403 — Model not allowed">
    Returned when the virtual key's `models` allow-list does not include the model you requested. Either add the model to the key's allow-list or use a key that grants access to that model.
  </Accordion>

  <Accordion title="402 — Budget exceeded">
    Returned when a spend budget at any scope (org, team, project, or key) has been exhausted for the current period. See [Budgets & Rate Limits](/security/budgets-and-rate-limits) to adjust or reset limits.
  </Accordion>

  <Accordion title="429 — Rate limit hit">
    Returned when the request or token rate for the key's scope chain has exceeded its per-minute cap. The response includes a `Retry-After` header indicating how many seconds to wait before retrying.
  </Accordion>
</AccordionGroup>

### Signing in to the dashboard

`POST /api/v1/auth/login` refuses with a stable `code` the dashboard renders
directly, so a failed sign-in says what went wrong instead of appearing to
succeed:

| Code                      | Status | What the sign-in screen shows                                        |
| ------------------------- | ------ | -------------------------------------------------------------------- |
| `invalid_credentials`     | 401    | that email and password do not match an account                      |
| `password_login_disabled` | 403    | the organization requires [single sign-on](/security/single-sign-on) |
| `too_many_attempts`       | 429    | how long the lock lasts, read from `Retry-After`                     |

The email-only gate — where the dashboard opens with no session at all — is
reserved for an open-mode control plane running with no store, where the auth
endpoints are not served. Anywhere else, a failed sign-in leaves you on the
sign-in screen with the reason on it.

### Staying signed in

A session token is valid for seven days. The dashboard re-checks the one it
stored every time it loads, with `GET /api/v1/auth/me`, and takes the answer
literally:

- **200** — the session is live. The account's superadmin flag and its
  memberships come from that answer, so the role shown in the sidebar and the
  organization the scope switcher opens on are the server's view, not a stale
  copy from the last sign-in.
- **401** — the session is over. The stored token is discarded and the sign-in
  screen appears with _"Your session expired — sign in again."_ Any other
  request that is refused the same way ends the session too, so an expired
  token cannot linger behind a screen that merely fails to load.
- **anything else** — a network failure, a 5xx, or a control plane that does
  not serve `/api/v1/auth/*` at all. The session is kept: a control plane that
  blinked is not a reason to sign you out.

### What ends a session early

A session ends before its seven days are up when:

- you sign out (`POST /api/v1/auth/logout`)
- a superadmin deactivates or deletes the account, or the identity provider
  deprovisions it over SCIM
- the account's second factor is reset
- the account changes its own password (`POST /api/v1/auth/password`): every
  other session ends and the one it was changed from stays. See
  [Change your password](/api/change-password)
- a superadmin sets a new password for the account (`PUT /api/v1/users/{id}`
  with a `password`). The usual reason for a reset is that the old password is
  in someone else's hands, so every session opened with it ends on the spot.
  The one exception is the session that made the request: a superadmin who
  resets their own password stays signed in on that page and is signed out
  everywhere else. The `user.update` audit entry records how many sessions the
  reset ended (`sessions_revoked`)
