Client keys
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.
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.1and its own address, and refuses requests with anotherHost; - 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.
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.
Set keys
Each entry is a key alone, or a table with a name:
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:
respan-gateway init --client-key # writes an sk-gw-... key into client_keys and prints it onceRESPAN_CLIENT_KEYS adds keys from the environment, comma separated, to any in the config. This is the easy way in containers:
RESPAN_CLIENT_KEYS=sk-gw-first,sk-gw-second respan-gateway --listen 0.0.0.0:8080A 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:
from openai import OpenAI
client = OpenAI(base_url="http://gateway:8080/v1", api_key="sk-gw-...") # Authorization: BearerA 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:
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"gw.requests(client="ci")Names also limit routes to some clients:
[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 and at /mcp, as server__tool globs:
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:
[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.