Skip to Content
Launching the open-source gateway

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.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.

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 once

RESPAN_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: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:

from openai import OpenAI client = OpenAI(base_url="http://gateway:8080/v1", api_key="sk-gw-...") # Authorization: Bearer

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:

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.