Flow
ServicesBroker Service

API

Relay, reported usage, savings, budgets, catalog, routing, and a person's own API credential.

Discovery and ops

PathMethodNotes
/GETService discovery (enveloped)
/api-docsGETSwagger UI
/openapi.yaml, /openapi.jsonGETSpec from binary
/healthzGETLiveness
/readyzGETReadiness (Postgres ping)

Versioned API

PathMethodContract
/v1/catalogGETProviders, models, limits, current prices. available=false when the key is missing
/v1/catalog/modelGETOne model by provider and wire_id, with its limits and price
/v1/peopleGETAuto-provisioned roster. q matches part of an email or name; people_only=true leaves out agents and service identities
/v1/peoplePOSTCreate or update a person (Foundry registers agents). creator_email names the person usage is grouped under
/v1/usagePOSTReport a completed call; requires Idempotency-Key
/v1/usageGETList by person, provider, model; from, to, actor, person, provider, model, group, by_creator
/v1/usage/summaryGETTotals over the same filters
/v1/savingsGETCache 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/defaultGET, PUTThe budget that applies without an override
/v1/budgetsGETEvery 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/groupsGETOne row per person with spend summed across the agents and service identities they created, and each identity under it
/v1/budgets/{actor}GET, PUT, DELETEOne person; DELETE clears the override
/v1/routing/rulesGET, PUTSmart-routing substitutions
/v1/routing/auto-routePUTTurn routing on or off for one email. It is on by default
/v1/routing/exceptionsGETPeople and services with routing turned off, with when and by whom
/v1/routing/settingsGETThe default rule: quick check limits, whether Jev is on, and the classifier model
/v1/routing/familiesGETEach route family with its enabled models, cheapest first
/v1/routing/statsGETAutomatic routing decisions by tier, classifier, family, outcome, and day; from, to, actor, family, classifier
/v1/relay/messagesPOSTAnthropic Messages
/v1/relay/chat/completionsPOSTOpenAI-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/speechPOSTMP3 for a short passage. The model and voice are fixed. Foundry calls this for Listen on Today. The response is audio/mpeg
/v1/relay/responsesPOSTOpenAI Responses
/v1/credentials/mineGET, POSTThe 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}/rotatePOSTNew 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}/disablePOSTStops 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}/enablePOSTTurns a disabled credential back on. The secret stays the same
/v1/credentials/mine/{client_id}/renamePOSTChanges 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}/tokenPOSTA one-hour token for that credential, without showing the secret again
/v1/oauth/tokenPOSTExchange client_id and secret for that token. Not an envelope. See Your own credential
/.well-known/jwks.jsonGETPublic 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

HeaderRequiredPurpose
Idempotency-KeyPOST /v1/usage and every PUT / DELETE on budgets or routingStable across retries
Content-TypePOST, PUT, DELETEapplication/json
X-Dev-Principallocal DEV_AUTH=1 onlyThe caller email
X-Flow-On-Behalf-Ofharness relayAgent mailbox; usage is billed to that email
X-Flow-Conversation-Idoptional, relay with an API credentialYour own id for the conversation, up to 200 characters. See Saved conversations
X-Flow-Knowledgeoptional, relay with an API credentialoff keeps the saved conversation out of the knowledge layer
Authorizationprogram callsBearer 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:

  1. X-Flow-Conversation-Id. Every call with the same value goes to one conversation.
  2. previous_response_id on a Responses call.
  3. conversation on a Responses call, OpenAI's own conversation id.
  4. 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.
  5. 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

StatusWhen
401Missing or invalid Access token (non-local), or a bad client secret on /v1/oauth/token
402Relay refused because the person is over budget
403Cedar denied the action
404Unknown person on GET /v1/budgets/{actor}
409Idempotency-Key replayed with a different body
422Unknown provider or model; bad period or limit
502The model provider rejected the request or could not be reached. The request log has the upstream status and body
503No 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.

On this page