API
Relay, reported usage, savings, budgets, catalog, routing, and a person's own API credential.
Discovery and ops
| Path | Method | Notes |
|---|---|---|
/ | GET | Service discovery (enveloped) |
/api-docs | GET | Swagger UI |
/openapi.yaml, /openapi.json | GET | Spec from binary |
/healthz | GET | Liveness |
/readyz | GET | Readiness (Postgres ping) |
Versioned API
| Path | Method | Contract |
|---|---|---|
/v1/catalog | GET | Providers, models, limits, current prices. available=false when the key is missing |
/v1/catalog/model | GET | One model by provider and wire_id, with its limits and price |
/v1/people | GET | Auto-provisioned roster. q matches part of an email or name; people_only=true leaves out agents and service identities |
/v1/people | POST | Create or update a person (Foundry registers agents). creator_email names the person usage is grouped under |
/v1/usage | POST | Report a completed call; requires Idempotency-Key |
/v1/usage | GET | List by person, provider, model; from, to, actor, person, provider, model, group, by_creator |
/v1/usage/summary | GET | Totals over the same filters |
/v1/savings | GET | Cache and routing savings, plus by_model with each baseline, and routing: routed calls, gross and net savings, grading cost, and savings by rule and by family and tier |
/v1/budgets/default | GET, PUT | The budget that applies without an override |
/v1/budgets | GET | Every known person with spend and effective limit. show=people leaves out agents and service identities; show=services lists only them. actor limits it to one person and what they created |
/v1/budgets/groups | GET | One row per person with spend summed across the agents and service identities they created, and each identity under it |
/v1/budgets/{actor} | GET, PUT, DELETE | One person; DELETE clears the override |
/v1/routing/rules | GET, PUT | Smart-routing substitutions |
/v1/routing/auto-route | PUT | Turn routing on or off for one email. It is on by default |
/v1/routing/exceptions | GET | People and services with routing turned off, with when and by whom |
/v1/routing/settings | GET | The default rule: quick check limits, whether Jev is on, and the classifier model |
/v1/routing/families | GET | Each route family with its enabled models, cheapest first |
/v1/routing/stats | GET | Automatic routing decisions by tier, classifier, family, outcome, and day; from, to, actor, family, classifier |
/v1/relay/messages | POST | Anthropic Messages |
/v1/relay/chat/completions | POST | OpenAI-compatible Chat Completions. provider selects the upstream and is stripped before the provider sees the body. See Relay for what happens when it is left out |
/v1/relay/speech | POST | MP3 for a short passage. The model and voice are fixed. Foundry calls this for Listen on Today. The response is audio/mpeg |
/v1/relay/responses | POST | OpenAI Responses |
/v1/credentials/mine | GET, POST | The signed-in person's credentials. POST creates another. The body is name, kind (personal, the default, or service), and expires_at when the credential should stop. Leave expires_at out for no expiration. The secret is returned once. There is no latest expiration |
/v1/credentials/mine/{client_id}/rotate | POST | New secret, shown once, and a one-hour token when the credential has not expired. Refused while the credential is disabled |
/v1/credentials/mine/{client_id}/disable | POST | Stops new tokens from that credential. Tokens already issued last until they expire. Rename and rotate are refused until it is enabled |
/v1/credentials/mine/{client_id}/enable | POST | Turns a disabled credential back on. The secret stays the same |
/v1/credentials/mine/{client_id}/rename | POST | Changes the name shown in the list. The body is name. The secret stays the same. Refused while the credential is disabled |
/v1/credentials/mine/{client_id}/token | POST | A one-hour token for that credential, without showing the secret again |
/v1/oauth/token | POST | Exchange client_id and secret for that token. Not an envelope. See Your own credential |
/.well-known/jwks.json | GET | Public keys for that token |
Enveloped All /v1/* JSON uses the Response envelope: The template service wraps every JSON response in {header, payload}. A success payload is {data} (a record or a list) plus optional meta.pagination; accounting still returns bare bodies and RFC 9457 errors until the divergence is resolved.
except relay, which returns the provider body as-is so a caller can treat broker as the
upstream, and POST /v1/oauth/token, which returns an OAuth token response.
List endpoints are page mode (ADR 0014).
GET /v1/usage defaults to group=day (one stored daily row). person_model,
person, model, and provider sum that rollup across the date range.
With by_creator=true, each service credential identity and Foundry agent mailbox is
summed into the person who created it, actor selects that person, and a row lists the
identities it sums in identities.
actor is an exact identity. person is a search: part of an email, a name, or the
first initial and last name run together (anead for AJ Nead), in any case. It also
matches every identity a matching person created, so a search for a person returns their
agents and service credentials too.
Authorization is Cedar: The authorization engine; each service embeds a Cedar policy and evaluates every mutating request against the actor's principal record.. Staff may relay, report usage, read the catalog,
read their own usage, and mint their own credential. A service token (Foundry, including the harness) may
POST /v1/people, GET /v1/catalog, and relay so an always-on agent can
evaluate its prompt. Platform admins may read every person, manage budgets, change
routing rules, list routing exceptions, and turn routing off for a person.
Headers
| Header | Required | Purpose |
|---|---|---|
Idempotency-Key | POST /v1/usage and every PUT / DELETE on budgets or routing | Stable across retries |
Content-Type | POST, PUT, DELETE | application/json |
X-Dev-Principal | local DEV_AUTH=1 only | The caller email |
X-Flow-On-Behalf-Of | harness relay | Agent mailbox; usage is billed to that email |
X-Flow-Conversation-Id | optional, relay with an API credential | Your own id for the conversation, up to 200 characters. See Saved conversations |
X-Flow-Knowledge | optional, relay with an API credential | off keeps the saved conversation out of the knowledge layer |
Authorization | program calls | Bearer plus a token from /v1/oauth/token. The caller is the email in the token. Basic on the token route itself, client id and secret |
Report usage
POST /v1/usage takes a command body:
{
"data": {
"provider_kind": "anthropic",
"model": "claude-sonnet-4-8",
"requested_model": "claude-opus-4-8",
"request_kind": "messages",
"input_tokens": 1200,
"output_tokens": 400,
"cache_read_tokens": 800
}
}requested_model is set when routing substituted a model. Broker prices the actual tokens
against that name to store routing_savings_micros.
actor_fid may be sent when reporting on someone else's behalf (admins). Otherwise the
authenticated caller is the actor.
Relay
Successful JSON responses from all three relay endpoints are provider objects with arbitrary
properties. Their OpenAPI response schemas declare that object shape explicitly, so the generated
Go client's WithResponse methods decode them as maps. The server forwards the provider bytes
unchanged; it does not wrap or re-encode them.
The body is the provider request. Broker may rewrite model when a routing rule matches,
or when automatic routing grades the request as needing less than the requested model.
Neither happens when routing is off for the caller. The provider response names the model
that ran. See Routing.
provider is a broker routing key and is stripped before the upstream call: OpenRouter
treats that field as its own object and returns HTTP 400 if it is a string.
On /v1/relay/chat/completions and /v1/relay/responses, provider may be left out.
Broker then uses openai if OpenAI offers that model, or else the one provider in the
catalog that does. If several other providers offer the same model, the call is 422
and the caller must set provider.
Over budget is 402 before any upstream call. A missing provider key is 503.
A rejected or unreachable provider is 502. The request log for that call includes
provider, model, actor, upstreamStatus, upstreamMessage, upstreamBody,
and error.
Relay is not a command. The upstream call is not replayable from a stored response, and the usage row is written after the provider replies.
Saved conversations
A call to /v1/relay/messages, /v1/relay/chat/completions or /v1/relay/responses made
with an API credential is saved to a workspace conversation after the reply is sent. The
conversation belongs to the credential's identity: the owner for a personal credential, or
<client_id>@service.broker for a service credential. It is not filed in a project, and it
feeds the knowledge layer unless the call sends X-Flow-Knowledge: off. Nobody signs in as a
service identity, so until a service credential's conversation is filed in a project only
platform admins can read it in workspace. Calls from Foundry,
the morning brief, or a browser session are not saved here. The decision is
docs/decisions/0103-broker-saves-api-credential-conversations.md.
Broker picks the conversation in this order:
X-Flow-Conversation-Id. Every call with the same value goes to one conversation.previous_response_idon a Responses call.conversationon a Responses call, OpenAI's own conversation id.- The longest list of earlier messages that a previous call ended with. Messages and Chat Completions resend the whole history on every call, so this usually finds the conversation without the header.
- Otherwise, a new conversation.
Only user and assistant text is saved, one turn per message. System prompts, tool
definitions, tool calls and tool results are left out. Messages the conversation already
has are not saved again. The reply's turn is authored agent:<model>, with the model and
provider in the turn's extra.
Broker writes with SERVICE_TOKEN and X-Flow-On-Behalf-Of, not with the caller's token.
Broker itself keeps only the link from a header value, response id or message hash to the
workspace conversation, in broker_conversations and broker_conversation_links. Saving
is best effort: if workspace fails, the error is logged and those turns are not saved. With
WORKSPACE_URL unset, nothing is saved.
Your own credential
A staff member creates credentials in the Broker workspace, on API access. Each
one has a name. An expiration is optional, and there is no latest expiration.
The secret is returned once. A program sends it with HTTP Basic to
POST /v1/oauth/token and grant_type=client_credentials. The JWT lasts one
hour. For a personal credential its email claim is the person's sign-in
address, copied from the credential row. For a service credential it is the
credential's own identity, <client_id>@service.broker, which has its own
budget. The token request cannot set a different email. After a set
expiration, the exchange is refused. With no expiration, it keeps working until
the credential is disabled. A new JWT is minted by exchanging the client secret
again.
The create, rotate, disable, enable, rename, and issue routes accept the person's Access session, or the local dev header. A broker JWT and the shared service token are refused there, so a leaked token cannot renew itself or replace the secret. Other broker routes still accept the JWT as that person.
Intelligence, workspace, and foundry accept that JWT as the same person. Relay bills that person. The decision is docs/decisions/0079-a-person-mints-their-own-api-credential.md.
Budgets
The default is one row. GET /v1/budgets joins people (email addresses only), the
rollup, and the two budget tables so tokens, spend, and limit for everyone is one
query. DELETE /v1/budgets/{actor}
removes the override; the person has no row of their own afterwards.
Each row has a kind: person, agent (a Foundry agent's mailbox), or service (a
service credential's identity). An agent or service row carries creator_fid and
creator_name, and the list puts it directly after the person who created it. It keeps
its own budget, separate from that person's. show=people leaves those rows out, and show=services lists only those rows.
actor=<fid> limits the list to that person and the identities they created.
GET /v1/budgets/groups pages by person instead of by identity. Each group is a person
with every agent and service identity they created: identity_count, and spend_micros
and token_count summed across all of them for the current period. members lists the
same rows GET /v1/budgets returns, the person's own row first. It takes the same
show and actor parameters. A group has no limit of its own; each member keeps its own
budget.
Spend made with a person's own API credential is billed to that person, so it is part of their own row. Usage records do not store which credential made a call, so that spend cannot be shown separately.
Errors
| Status | When |
|---|---|
401 | Missing or invalid Access token (non-local), or a bad client secret on /v1/oauth/token |
402 | Relay refused because the person is over budget |
403 | Cedar denied the action |
404 | Unknown person on GET /v1/budgets/{actor} |
409 | Idempotency-Key replayed with a different body |
422 | Unknown provider or model; bad period or limit |
502 | The model provider rejected the request or could not be reached. The request log has the upstream status and body |
503 | No credential configured for that provider, or token signing is not configured |
OpenAPI
Generated from the Go handler types and served at runtime. The merged platform spec
excludes broker today (DEPLOYED_SERVICES in the Makefile).
Source: services/broker/openapi.yaml.