Control Tower v0.2.1

API reference

Control Tower serves three APIs on one port:

  • the gateway, which agents call with their own key;
  • the admin API, which the console uses and you can script;
  • health and metrics endpoints for your platform.

Authentication

CallerCredentialHeader
Agents (gateway)The agent's key, ct_sk_…Authorization: Bearer, x-api-key or api-key; x-ct-key for HTTP APIs
Scripts (admin API)The admin keyAuthorization: Bearer <admin key>
The console (admin API)A session cookie from POST /admin/api/logincookie plus x-ct-csrf: <token from /admin/api/me> on writes
PrometheusCT_METRICS_TOKEN (or any console session, or the admin key)Authorization: Bearer
export CT=http://localhost:4000
export ADMIN="Authorization: Bearer $CT_ADMIN_KEY"
curl -s $CT/admin/api/keys -H "$ADMIN"

Errors are JSON: {"error": {"code": "…", "message": "…"}} (the gateway uses the envelope of the API the client speaks). An unexpected failure is 500 internal_error, with a message that gives the request id to look up in the server's log. Model, HTTP API and A2A responses carry x-ct-flight-id; MCP responses, /v1/models and /v1/messages/count_tokens don't. Every code is listed in Troubleshooting.

Gateway

MethodPath
POST/v1/chat/completionsOpenAI Chat Completions, streaming or not — to any provider (translated where needed)
POST/v1/responsesOpenAI Responses API (Agents SDK, Codex) — to any provider (translated through Chat Completions where needed)
POST/v1/embeddingsEmbeddings
POST/v1/images/generations, /v1/images/edits, /v1/images/variationsImages (model APIs); edits and variations as multipart uploads
POST/v1/audio/speech, /v1/audio/transcriptions, /v1/audio/translationsSpeech and transcription; transcriptions as multipart uploads
POST/v1/moderations, /v1/rerank (/v2/rerank), /v1/completionsModeration, rerank, legacy completions
POST/gemini/{version}/models/{model}:{method}Gemini's own API (key as x-goog-api-key or ?key=)
POST/bedrock/model/{modelId}/{operation}Bedrock's runtime API: converse, converse-stream, invoke, invoke-with-response-stream
GET/v1/models, /v1/models/:idThe models this key may use
POST/v1/messagesAnthropic Messages API (Claude Code, Anthropic SDKs)
POST/v1/messages/count_tokensToken counting — exact from Anthropic, estimated elsewhere
POST/openai/deployments/:model/chat/completions, …/embeddingsAzure OpenAI style
POST, DELETE/mcp, /mcp/:slugMCP (Streamable HTTP): every server, or one. GET answers 405: Control Tower doesn't open server-initiated streams
any/http/:slug/*A registered HTTP API
GET/a2aThe A2A agents this key may reach
GET/a2a/:slug/.well-known/agent-card.jsonAn A2A agent's card, pointing at Control Tower
POST/a2a/:slugA2A JSON-RPC: SendMessage, GetTask, … (1.0) or message/send, … (0.3)
POST/v1/delegation/renewRenew a delegation token (x-ct-delegation header or {"token"}) for the agent it was issued to: {token, expires_at} — see Agents calling agents
POST/v1/observeReport calls made outside the gateway
POST/v1/tracesOpenTelemetry traces (OTLP/HTTP JSON)

The OpenAI routes also answer without /v1. Requests may carry x-ct-tags, x-ct-customer, x-ct-region and x-ct-cache (tags and customers, caching), a W3C traceparent (Control Tower's spans join the agent's trace), x-ct-delegation (agents calling agents) and x-ct-session.

Requests held for approval are retried with x-ct-approval: <ticket> (MCP also takes params._meta.ct_approval, A2A params.metadata.ct_approval); the HTTP gateway also returns the ticket in an x-ct-approval-ticket header. On model calls, x-ct-session (or the request's user or metadata.user_id) names the agent's session: a duplicate retry within 5 seconds with the same ticket and session goes through on the same approval instead of asking for a new one. See Approvals.

Admin API

All paths are under /admin/api. Approvers and viewers may read any of them (except /users and /audit); only admins change anything, except that approvers decide approvals and everyone may change their own password.

Session

MethodPath
GET/status{setup_complete} without a session; signed in (or with the admin key), also the version and more
POST/setupFirst run: create the admin ({email, password, setup_code}). 403 setup_code without the setup code from the server's log, 409 already_setup once done; 10 attempts a minute per address
POST/loginStart a console session ({email, password}); CT_LOGIN_RPM attempts a minute per email, twice that per address (429 rate_limited)
POST/logoutEnd it
GET/meThe signed-in person, their role and the CSRF token
POST/me/passwordChange your own password ({current, password})
GET, POST/usersPeople who sign in, and their roles (admins only); add one — see People and roles
PATCH, DELETE/users/:idChange a role, reset a password (a new one-time password), remove
GET, POST, PATCH, DELETE/identity-providers…, /sso/settingsSingle sign-on (OIDC and SAML) and SCIM tokens (admins; Enterprise)
GET/audit, /audit/export, /audit/verifyThe audit log (admins only): browse with filters, download as CSV or JSON Lines, check its hash chain

Providers, models, aliases

MethodPath
GET/catalogProvider catalogue and credential fields
GET, POST/providersList, connect ({catalog_id, name?, slug?, base_url?, credentials})
PATCH, DELETE/providers/:idUpdate, remove
POST/providers/:id/testTest the connection
GET/providers/:id/modelsModels the provider offers
GET, POST/deploymentsList, add ({provider_id, upstream_model, public_name?, pricing_override?, caps?})
POST/deployments/check, /deployments/:id/checkHealth-check every model now, or one with a real one-token call
DELETE/cacheForget every cached answer
PATCH, DELETE/deployments/:idUpdate (enable, rename, price, caps: region, tags, context, rpm, tpm, max_parallel, headers_timeout_ms, health_probe (health-check with a real one-token call), mode (embedding for an embeddings model), and for a deployment called by name fallbacks, retry, cache; null clears one), remove
GET, POST/aliasesList, add ({name, strategy, targets: [{deployment_id, priority, weight}], config?}); config: {fallbacks: {context_window, content_policy, default}, retry: {rate_limited, timeout, server_error, unreachable, max_attempts}, cache: {ttl_s, shared}} — see retries and fallback models
PUT, DELETE/aliases/:idReplace, remove
GET/pricingThe price table

Keys and budgets

MethodPath
GET, POST/keysList, create — see fields. The secret is returned once
PATCH, DELETE/keys/:idUpdate limits, budget, expiry, enable / disable; delete
POST/keys/bulkDisable or delete many keys: {action: "disable" | "delete", ids: [...]} (up to 5,000; Control Tower's own keys are skipped)
GET, PUT/keys/retire-policyRetire keys unused for N days: {idle_days: 0 | 7 | 30 | 90} (0 is never); a PUT retires what is already idle and lists it
GET/budgetsEvery budget with spend, and the known teams and projects
PUT, DELETE/budgets/:type/:idSet or remove a key, team, project or customer budget ({limit_usd, period, hard})
GET/customers?window=24h|7d|30dEnd customers with their requests, agents, spend and budget (tags and customers)
PUT, DELETE/customers/:idName, block or unblock a customer ({name?, blocked?, note?}); forget it
GET/ledger/tags?window=…Spend by the tags requests carried

Tool servers

MethodPath
GET, POST/mcp/serversList, register ({name, slug, url, auth?: {type: "bearer", token} | {type: "headers", headers}})
PATCH, DELETE/mcp/servers/:idUpdate, remove
POST/mcp/servers/:id/testConnect and refresh the tool list
GET, POST/http/apisList, register an HTTP API ({name, slug?, base_url, auth?: {type: "bearer", token} | {type: "header", header, token}, agent_id?})
PATCH, DELETE/http/apis/:idUpdate, remove
POST/http/apis/:id/testCheck it is reachable
GET, POST/a2a/agentsList, register an A2A agent ({name, slug?, url, auth?, agent_id?}; url is its card or base URL)
PATCH, DELETE/a2a/agents/:idUpdate, remove
POST/a2a/agents/:id/testRead its card again

Policy and approvals

MethodPath
GET/policyZones, gates, enforcement state and approval stats
POST, PATCH, DELETE/zones, /zones/:idZones
POST, PATCH, DELETE/rules, /rules/:idGates
POST/policy/simulateReplay recent traffic through a draft gate
GET/policy/exportPolicy as YAML (?format=json for JSON)
POST/policy/importPreview or apply a policy file ({yaml, mode, apply})
GET/guardrails/detectorsDetectors available to inspect gates
GET/approvals, /approvals/:idApproval requests (?status=pending)
POST/approvals/:id/decide{action: "approve" | "deny", note?}
GET/grantsGrants issued by approvals
GET/approval-windowsOpen approval windows, with calls and time left
POST/grants/:id/revokeRevoke a grant before it is used, or end a window (admins only)

Exports

MethodPath
GET, POST/exportsList destinations with delivery counts; add one ({name, kind: otlp | datadog | splunk | s3 | webhook, config}) — see Exporting flights
PATCH, DELETE/exports/:idRename, pause (enabled), change settings (secrets left out are kept); remove
POST/exports/testSend an example record: {id} for a saved destination, or {kind, config}
POST/exports/:id/flushSend what is waiting now

Guardrail services

MethodPath
GET, POST/guardrail-servicesList (with the gates using each), add ({name, kind, config}) — see Guardrail services
PATCH, DELETE/guardrail-services/:idChange (secrets left out are kept), turn off, remove (refused while a gate uses it)
POST/guardrail-services/testTry a service on a text

Alerts

MethodPath
GET, POST/alert-rulesList, create — see the body
PATCH, DELETE/alert-rules/:idUpdate, remove
GET, POST/alert-channelsList, create (slack, webhook, email)
PATCH, DELETE/alert-channels/:idUpdate, remove
POST/alert-channels/:id/testSend a test message
GET/alertsThe inbox
POST/alerts/readMark alerts read

Traffic, spend and the map

MethodPath
GET/flightsRecent flights (?limit, before, status, key_id, kind; for=<agent id>: calls made on behalf of that agent anywhere up the chain; trace=<flight id>: every call in the same chain, from the call that started it to everything it led to). Each flight has parent_flight_id (the call that led to it) and has_children
GET/flights/:idOne flight with its events
GET/events/recentThe latest flight events
GET/replayFlights in a window, compact, for replay (?from, to in epoch ms)
GET/ledger/summarySpend, requests and tokens by key and model (?window=1h|24h|7d|30d)
GET/topologyEverything on the map: keys, models, servers, views, connections per agent and team (gzipped when accepted)
GET, PUT/airspace/layoutThe saved arrangement of the map
GET/airspace/agent-link?from=&to=&since=The calls behind an arc between two agents: from and to are comma-separated key ids, since epoch ms (default: 7 days). The caller's calls to the servers that front the callee, what each led to, and what the callee did on the caller's behalf
GET, POST/airspace/viewsViews: {name, teams, color?}
PATCH, DELETE/airspace/views/:idChange or remove a view
GET/export/dataflowThe data-flow inventory (?format=md|csv, hours)
GET (WebSocket)/admin/wsLive traffic for the console: a tick each second (totals, calls per path, gate hits) and events for held, denied and failed flights. Needs a session; a browser from another origin is refused (close code 4403)

Setup helpers

MethodPath
POST/import/config/plan, /import/config/applyImport a config file once
POST, DELETE/demoStart or stop the demo fleet
POST/playground/chatSend a request from the console's playground

Key and model management API

With the admin key: POST /key/generate, GET /key/info, POST /key/update, GET /key/list, POST /key/delete, POST /key/block, POST /key/unblock, POST /key/regenerate (also /key/:key/regenerate), GET /model/info (also /v1/model/info), POST /model/new, POST /model/delete. See Keys.

POST /model/new takes one entry in the form of a config file's model_list: {model_name, params: {model: "<provider>/<model>", api_key?, api_base?, …}, model_info?}. Credentials it leaves out are read from the usual environment variables; it reuses a provider with the same endpoint and credentials, and returns the new model's model_id. POST /model/delete takes {id}.

SCIM 2.0

/scim/v2/Users, /scim/v2/Groups, /scim/v2/ServiceProviderConfig, /scim/v2/ResourceTypes, /scim/v2/Schemas, with an identity provider's SCIM token as a bearer token (Enterprise). See SCIM provisioning.

Health and metrics

MethodPath
GET/healthz, /health/liveliness, /health/livenessLiveness
GET/readyz, /health/readinessReadiness. /readyz answers {ok, shutting_down}, and with the admin key also the event backlog, database and provider counts
GET/healthEvery connected provider checked (admin or agent key)
GET/metricsPrometheus: CT_METRICS_TOKEN, any console session or the admin key — see Monitoring
GET/uiRedirects to the console