---
title: "Client keys"
subtitle: "Keys your apps send to the gateway. Required before it listens beyond localhost, and the name each request is logged under."
---

A client key is a key you make up and give to your apps. The gateway checks it, removes it from the request, and calls providers with its own provider keys. No provider ever sees a client key.

```toml
client_keys = [
  { name = "app", key = "env:APP_KEY" },
  { name = "ci",  key = "env:CI_KEY" },
]
```

With `client_keys` set, every request must carry one of them. A request without a valid key gets 401.

## Why you need them

Without client keys, anyone who can reach the gateway spends your provider keys. So a gateway with no client keys:

- refuses to start on a non-loopback address such as `0.0.0.0:8080`;
- answers only to `localhost`, `127.0.0.1` and its own address, and refuses requests with another `Host`;
- refuses cross-origin requests from browsers, so a web page you open can't call it.

Set client keys before you run the gateway on a server, in Docker, or anywhere other machines can reach it.

<Note>
The Python package's `gw.serve()` does not refuse a non-loopback address. If you serve from Python on anything other than `127.0.0.1`, set `client_keys` yourself.
</Note>

## Set keys

Each entry is a key alone, or a table with a name:

```toml
client_keys = [
  "env:SHARED_KEY",                                            # logged as key-1
  { name = "app", key = "env:APP_KEY" },
  { name = "ops", key = "env:OPS_KEY", admin = true },
  { name = "bot", key = "env:BOT_KEY", tools = ["search__*"] },
]
```

Write keys as `env:NAME` to keep them out of the file. To generate a strong key into a new config:

```bash
respan-gateway init --client-key     # writes an sk-gw-... key into client_keys and prints it once
```

`RESPAN_CLIENT_KEYS` adds keys from the environment, comma separated, to any in the config. This is the easy way in containers:

```bash
RESPAN_CLIENT_KEYS=sk-gw-first,sk-gw-second respan-gateway --listen 0.0.0.0:8080
```

A key without a name is logged as `key-<n>`, by its place in the list. Keys from `RESPAN_CLIENT_KEYS` have no name and come after the config's, so with no keys in the config they are `key-1`, `key-2`, and so on. `RESPAN_CLIENT_KEYS` is read by the `respan-gateway` command; the Python `rg.Gateway()` does not read it.

## Send a key

The gateway accepts a client key in any of these headers:

- `Authorization: Bearer <key>`
- `x-api-key: <key>`
- `x-goog-api-key: <key>`

So every SDK works by passing the client key as its API key:

<Tabs>
<Tab title="OpenAI">
```python
from openai import OpenAI

client = OpenAI(base_url="http://gateway:8080/v1", api_key="sk-gw-...")   # Authorization: Bearer
```
</Tab>
<Tab title="Anthropic">
```python
from anthropic import Anthropic

client = Anthropic(base_url="http://gateway:8080", api_key="sk-gw-...")   # x-api-key
```
</Tab>
<Tab title="Google GenAI">
```python
from google import genai

client = genai.Client(api_key="sk-gw-...", http_options={"base_url": "http://gateway:8080"})   # x-goog-api-key
```
</Tab>
<Tab title="curl">
```bash
curl http://gateway:8080/v1/chat/completions \
  -H "Authorization: Bearer sk-gw-..." \
  -H "content-type: application/json" \
  -d '{"model": "gpt-6-luna", "messages": [{"role": "user", "content": "hi"}]}'
```
</Tab>
</Tabs>

A browser can't set headers on a WebSocket, so a realtime session from a browser may send the key as a subprotocol (`openai-insecure-api-key.<key>`) or as a query parameter (`?key=<key>`).

If a request carries a client key in one header and the client's own provider key in another, the gateway removes only the client key. The provider key goes on to a provider that has no key of its own.

## Names in the request log

Each request is logged under its key's name. Filter by it in the web UI, the admin API, or Python:

```bash
curl "http://localhost:8080/ui/api/requests?client=ci" -H "Authorization: Bearer $OPS_KEY"
curl "http://localhost:8080/ui/api/requests/export?client=ci&format=csv" -H "Authorization: Bearer $OPS_KEY"
```

```python
gw.requests(client="ci")
```

Names also limit [routes](/docs/oss-gateway/routes#limit-a-route) to some clients:

```toml
[routes.internal]
to   = "openai/gpt-6-luna"
when = { clients = ["ops"] }
```

## Admin keys

The admin API under `/ui/api/` (the request log, route explain, tool switches, reload) needs a client key once keys are set. If no key sets `admin`, every key reaches it. Once any key sets `admin = true`, only those keys do; the others get 403.

## Limit a key to some tools

`tools` limits what a key's requests can use, in the [tool loop](/docs/oss-gateway/tools#tool-loop) and at `/mcp`, as `server__tool` globs:

```toml
client_keys = [
  { name = "bot",   key = "env:BOT_KEY",   tools = ["search__*"] },
  { name = "plain", key = "env:PLAIN_KEY", tools = [] },          # no tools at all
]
```

Unset means every tool. A key with `tools` set reaches MCP servers only through `/mcp`, which holds it to the list. `/mcp/{name}` refuses it with 403 `tools_limited`, since that endpoint offers all of a server's tools.

## Browser apps

Web pages on other origins also need a `[cors]` entry:

```toml
[cors]
origins = ["https://app.example.com"]
```

Their preflight requests are answered without a key; their other requests need one as usual. `origins = ["*"]` is allowed only with client keys.
