Authentication & keys
Every gateway request authenticates with a Relay API key using the ApiKey scheme. The only exceptions carry a credential of their own: GET /health, the realtime WebSocket (a single-use session token), and workflow webhook / resume URLs (the token in the URL).
Authorization: Bearer app_live_xxxxxxxxxxxxxxxxxxxxxxxx
#Issuing keys
Create keys in the control panel under API Keys. The full key is shown once at creation — copy it then. Keys belong to the workspace you have selected, which is how usage, budgets and cost attribution are grouped; it is stamped server-side and never typed in.
Manage keys via the control-plane API too: POST /api/ApiKeys/search, POST /api/ApiKeys, PUT /api/ApiKeys/{id}, POST /api/ApiKeys/{id}/revoke, DELETE /api/ApiKeys/{id}.
#What a key controls
Everything below is settable at creation and editable afterwards:
| Setting | Effect | Empty / unset means |
|---|---|---|
| Name | Identifies the key in the panel and in cost attribution. | — (required) |
| Scopes | Capabilities the key may exercise — see the table below. A request outside the list returns 403 insufficient_scope. | Every scope allowed. |
| Model allow-list | Models the key may call. A request for anything else returns 403 model_not_allowed; GET /v1/models lists only these. | Any enabled model. |
| Rate limit | Requests per fixed 60-second window, counted per gateway instance. Over the limit returns 429 rate_limit_exceeded with Retry-After; every response from a limited key carries X-RateLimit-Limit / X-RateLimit-Remaining. | No per-key limit. |
| Expiry | The key stops working after this date (end of day). | Never expires. |
| Environment | A default environment stamped on this key's traces (production, staging, …) when the request sends none. | Unset. |
| Trace body capture | none / metadata / full for this key's traces, overriding the workspace default. A caller can still lower it per request. See Observability. | Workspace default. |
An empty scope list or allow-list means unrestricted, not nothing — so adding a new scope to the gateway never retroactively locks out existing keys.
#Scopes
| Scope | Grants |
|---|---|
chat.completions | POST /v1/chat/completions. |
embeddings | POST /v1/embeddings. |
models.read | GET /v1/models and GET /v1/models/metrics. |
workflows.run | Everything under /v1/workflows except webhook and resume URLs. |
data.query | Run a dataset agent, which queries the data app as the named end user. |
connectors.use | Run a connector agent, which reads the named end user's Microsoft 365 account. |
telemetry.read | Read this workspace's traces, sessions, users, scores and metrics (/v1/telemetry/*, GET /v1/scores, POST /v1/metrics) — metadata only. |
telemetry.bodies | Also return captured prompts and completions when reading telemetry. |
scores.write | Attach scores and feedback (POST /v1/scores, /batch, DELETE). |
traces.write | Send app-side spans (POST /v1/traces/ingest, /v1/otlp/v1/traces). |
Scopes gate the paths they name; the rest of the API (prompts, agents, files, knowledge, batches, realtime, tools, pricing, /v1/responses) needs only a valid key. data.query and connectors.use are checked inside the agent run, because gating /v1/agents as a whole would gate every agent, dataset or not. The two are kept apart from chat.completions deliberately: both act on a caller-asserted end user that Relay cannot verify, so a key holding them can read any end user's data by varying the header. Grant them only to applications that authenticate their own users.
#Editing a key
A key's details can be changed at any time; the secret cannot. Editing changes the name, scopes, allow-list, rate limit, expiry, environment and capture level, and leaves the key its holder is using working exactly as before.
There is deliberately no way to rotate a key in place. Repointing an existing key at a new secret would either break every caller silently or, worse, leave two secrets believed to be one. Rotation is revoke + issue a new key, which leaves an audit trail that an edit would not.
Nor is the key retrievable. Only a SHA-256 hash is stored, so after creation the panel can show the identifiable prefix (app_live_xxxxxxxx…) and nothing more — not because it is being withheld, but because the rest was never kept.
#Budgets
A workspace has one spend ceiling, optionally divided into per-member allocations. When workspace spend reaches a hard-stop cap, the router rejects further requests with 402 budget_exceeded before anything is sent upstream. A separate per-end-user limit (keyed by X-Relay-User-Id) returns 402 end_user_budget_exceeded. Spend is recomputed periodically from telemetry (Gateway:BudgetRefreshMinutes, default 5), so a stop engages within that window rather than instantly. See Budgets & spend limits.
#Control-panel authentication
The control panel is a separate app that authenticates admins through the company's Flowbyte identity app (OAuth sign-in, then a cookie) — not API keys. The api/* control-plane endpoints require a signed-in admin session; the public /v1/* API requires an API key. Keep the two mental models separate:
Gateway API (/v1/*) | Control panel (api/*) | |
|---|---|---|
| Who | Applications | Admins / team owners |
| Auth | API key (Bearer) | Identity-app sign-in (cookie) |
One bridge exists between them, by design: the panel's Playground runs a call as one of your stored keys without ever holding its secret. It posts to the panel host, which forwards to the gateway with a two-minute signed delegation token that names the key id; the gateway then treats the call as that key's in every respect. Only the Playground's four routes are forwarded.
#Good practice
- One key per app or environment, so you can revoke narrowly.
- Set a model allow-list on keys that only need a specific model.
- Put every app's key behind the team that should own its cost.
- Keep
data.query,connectors.useandtelemetry.bodiesoff keys that do not need them.