# Switchboard — full developer docs > Concatenated from docs/*.md for agent one-shot context. Prefer /llms.txt + selective fetches when possible. ===== FILE: docs/README.md ===== # Switchboard developer docs > One hub key. Provider secrets stay in the cell. Response `data` is still the third party’s shape — Switchboard wraps it with `meta` (cost, price, request id, outcome). **For coding agents:** start at [`/llms.txt`](../marketing/public/llms.txt) (or this folder’s [`llms.txt`](./llms.txt)), then load [`openapi.yaml`](./openapi.yaml). Prefer Markdown pages linked from `llms.txt` over scraping HTML. ## Quick start 1. Mint a caller key in Control Room (scopes = connectors / models you need) and publish LKG. 2. Set env: ```bash SWITCHBOARD_BASE_URL=https://us1.switchboard.example # no trailing slash SB_KEY=sb_live_… # or SWITCHBOARD_API_KEY ``` 3. Call (hub-native): ```bash curl -sS "$SWITCHBOARD_BASE_URL/v1/call/twilio.send_sms" \ -H "Authorization: Bearer $SB_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: sms-1" \ -d '{"To":"+15550192","Body":"hi"}' ``` 4. Or use thinking tiers (recommended for product AI): ```bash curl -sS "$SWITCHBOARD_BASE_URL/v1/proxy/switchboard/chat/completions" \ -H "Authorization: Bearer $SB_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"thinker","messages":[{"role":"user","content":"hi"}],"max_tokens":64}' ``` ## Choose a mode | Mode | Path | When | |---|---|---| | Thinking tiers (A0) | `/v1/proxy/switchboard/…` | One model knob: `anybody` / `thinker` / `expert` | | Provider proxy (A) | `/v1/proxy/{connector}/…` | Keep Anthropic / OpenAI / Gemini / Mistral SDKs | | Hub-native (C) | `/v1/call/{connector}.{op}` | Stripe, Twilio, Buffer, HubSpot, … | | MCP | `POST /v1/mcp` | Agents / tool loops | | OpenAI-compat (B) | `/v1/chat/completions` | Smoke only — raw JSON + `SB-*` headers | ## Doc map | Page | Audience | |---|---| | [authentication.md](./authentication.md) | Keys, OAuth, scopes | | [call-modes.md](./call-modes.md) | Modes A0/A/B/C + proxy baseURL rules | | [envelope.md](./envelope.md) | `data` / `meta` / `error` | | [headers.md](./headers.md) | `SB-*`, idempotency, region | | [errors.md](./errors.md) | Error codes + retry rules | | [mcp.md](./mcp.md) | Agent JSON-RPC tools | | [sdk-typescript.md](./sdk-typescript.md) | `@switchboard/sdk` | | [agents.md](./agents.md) | How coding agents should use these docs | | [changelog.md](./changelog.md) | API / envelope change log and deprecation policy | | [openapi.yaml](./openapi.yaml) | Machine-readable API contract | ## Hard rules (do not skip) - Apps authenticate **only** with Switchboard. Do not put OpenAI / Stripe / Twilio keys in product repos when the hub owns the hop. - Do **not** point Anthropic / Google / Mistral SDKs at `{cell}/v1` (OpenAI-compat). Use `/v1/proxy/{connector}`. - Do **not** double version prefixes on proxy `baseURL` (see [call-modes.md](./call-modes.md)). - On envelope errors, `data` is `null` and `error` is set — never treat “has meta” as success. - Side-effect ops need `Idempotency-Key`. ===== FILE: docs/authentication.md ===== # Authentication > All caller traffic uses `Authorization: Bearer `. The token is a Switchboard key (`sb_…`) or a short-lived JWT from customer OAuth. ## Keys | Kind | Prefix (typical) | Use | |---|---|---| | App | `sb_live_app_…` / `sb_test_app_…` | First-party backends | | Agent | `sb_live_agt_…` / `sb_test_agt_…` | MCP / tool loops; approvals on side effects | | Customer | `sb_live_cus_…` / `sb_test_cus_…` / OAuth client | External developers | | Sys | `sb_live_sys_…` | Platform operators / console BFF (admin routes only) | ### Live vs test ring The second segment of the token is the **ring**: | Ring | Prefix | Behavior | |---|---|---| | `live` | `sb_live_…` | Real provider egress, Axis billing, prepaid credits | | `test` | `sb_test_…` | Sandbox / simulator — no live Axis billing; connectors with a `sandbox` (or `test`) egress region are forced there; otherwise the cell’s `SB_SIMULATOR_URL` / `-simulator` base is used; virtual LLM ops prefer the `echo` harness | Mint with `env: "test"` (Control Room toggle, control plane `POST /v1/keys`, or `mintkey -env test`). Local smoke token: `sb_test_app_sandbox_devsecret01`. Mint in **Control Room** (or control plane `POST /v1/keys`), store the one-time secret, then **publish LKG** so cells load it. Scopes decide which connectors and models the key may hit (`openai.*`, `stripe.*`, `switchboard` + `anybody|thinker|expert`, family scopes like `ai.*`). Out of scope → `403` `forbidden_scope`. ## Request ```http Authorization: Bearer sb_live_… # or Authorization: Bearer sb_test_… ``` Optional attribution headers: `SB-Feature`, `SB-Customer`, `SB-Region` — see [headers.md](./headers.md). ## Customer OAuth (client credentials) ```bash curl -sS "$SWITCHBOARD_BASE_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id=key_cus_test \ -d client_secret=… # → { "access_token", "token_type":"Bearer", "expires_in": 900 } ``` Use the JWT as the Bearer token. It inherits the customer key’s scopes / namespace. ## Local smoke key With `make run-gateway`, mint or load a key from `platform/testdata/keys/` (never commit production secrets). Shape: ```text sb_live_app__ ``` Local testdata scopes are wide — do not reuse those secrets in production. ## What callers must never hold When Switchboard owns the hop: OpenAI, Anthropic, Stripe, Twilio, Buffer, … provider secrets stay in Vault / cell secrets (house or BYOK). Product repos only need `SB_KEY` + cell URL(s). ===== FILE: docs/call-modes.md ===== # Call modes > Pick by how much vendor SDK surface you want to keep. Auth is always Switchboard Bearer. ## Mode A0 — Thinking tiers (recommended for product AI) ```http POST /v1/proxy/switchboard/chat/completions Authorization: Bearer sb_… Content-Type: application/json { "model": "thinker", "messages": [{ "role": "user", "content": "…" }] } ``` | Model id | Intent | |---|---| | `anybody` | Cheap / fast ladder | | `thinker` | Default product AI | | `expert` | Highest quality ladder | Gateway walks the configured ladder, injects house (or BYOK) credentials, bills **price** from `pricebook://switchboard/chat/{tier}`, and records **cost** from the resolved provider’s pricebook row. See `meta.resolved_connector` / `meta.resolved_model`. SDK helper: `switchboardTiersBaseUrl(cell)` → `{cell}/v1/proxy/switchboard`. ## Mode A — Per-provider proxy Keep Anthropic / OpenAI / Gemini / Mistral / xAI SDKs. Set `baseURL` to `{cell}/v1/proxy/{connector}` and auth with the Switchboard key (use `createProxyFetch` to unwrap envelopes). | Provider | SDK `baseURL` | SDK appends | Full path | |---|---|---|---| | Switchboard tiers | `{CELL}/v1/proxy/switchboard` | `/chat/completions` | `/v1/proxy/switchboard/chat/completions` | | Anthropic | `{CELL}/v1/proxy/anthropic` | `/messages` | `/v1/proxy/anthropic/messages` | | OpenAI | `{CELL}/v1/proxy/openai` | `/chat/completions`, `/embeddings`, … | `/v1/proxy/openai/…` | | Google | `{CELL}/v1/proxy/google` | `/models/{id}:generateContent` | `/v1/proxy/google/models/…` | | Mistral | `{CELL}/v1/proxy/mistral` | `/chat/completions` | `/v1/proxy/mistral/chat/completions` | ### Critical — do not double the version prefix Vendor connectors already include `/v1` or `/v1beta` in `base_url`. If you set Anthropic `baseURL` to `{CELL}/v1/proxy/anthropic/v1`, the SDK appends `/messages` and the upstream path breaks. **Wrong:** `{CELL}/v1/proxy/anthropic/v1` **Right:** `{CELL}/v1/proxy/anthropic` JSON responses are Switchboard envelopes. SSE streams stay raw; hub facts on `SB-*` headers. `createProxyFetch` unwraps success `data` for vendor SDKs. ## Mode C — Hub-native call ```http POST /v1/call/{connector}.{op} ``` Named ops from `connector.yaml` (e.g. `stripe.create_payment_intent`, `twilio.send_sms`, `buffer.create_post`). No vendor SDK required. Always enveloped for JSON. Side-effect / hub-idempotent ops require `Idempotency-Key`. ## Mode MCP — Agents ```http POST /v1/mcp ``` JSON-RPC `tools/list` + `tools/call`. See [mcp.md](./mcp.md). ## Mode B — OpenAI-compat (narrow) ```http POST /v1/chat/completions POST /v1/embeddings ``` Returns **raw** OpenAI JSON + `SB-*` headers. Pinned to gateway `-llm` (often `echo` locally). Do **not** use for multi-provider products; do **not** point non-OpenAI SDKs here. ## Decision guide | If you… | Use… | |---|---| | Want one model knob for cost/quality | Thinking tiers | | Already have Anthropic/OpenAI/Gemini SDKs | Provider proxy + `createProxyFetch` | | Need Stripe / Twilio / Buffer / HubSpot / … | `/v1/call/…` | | Building an agent with tools | `POST /v1/mcp` | | Customer brings their own provider key | BYOK capture link (config) — same `SB_KEY` on calls | ===== FILE: docs/envelope.md ===== # Response envelope > Callers speak Switchboard at the edge; the payload they care about is still the provider’s. JSON ops and proxy return `{ data, meta, error? }`. Streams and OpenAI-compat are raw + `SB-*` headers. ## Shape ```json { "data": { }, "meta": { "schema": "v1", "request_id": "req_…", "outcome": "sent", "cache": "miss", "provider_account": "house-primary", "units": { "tokens": 168 }, "cost": { "amount": 0.002, "currency": "USD" }, "price": { "amount": 0.005, "currency": "USD" }, "latency_ms": { "hub": 1.2, "provider": 180 } } } ``` | Field | Owner | Meaning | |---|---|---| | `data` | Third party | Stripe PaymentIntent, chat completion, GraphQL payload, … | | `meta` | Switchboard | request id, outcome, who paid the provider key, units, cost/price, latency, resolved model for tiers | | `error` | Switchboard | Hub/policy failure. On errors `data` is present and `null` | `error` fields: `code`, `message`, `retryable`, `request_id`, and optionally `retry_after_ms` (also mirrored as HTTP `Retry-After` in seconds). See [errors.md](./errors.md). JSON Schema: `platform/schemas/envelope.schema.json`. ## Outcomes `meta.outcome`: `sent` | `failed` | `unknown` | `pending` Do **not** treat “object has `data` and `meta`” as success — check `error` / `outcome`. ## Cost vs price Quoted from the cell price book after metering: - **cost** — what the provider charges you (`pricebook://…` cost) - **price** — what you charge the caller (`pricebook://…` price) Connector ops declare `meter.cost_ref` (e.g. `pricebook://twilio/sms/{to_country}`). Thinking tiers bill list price from `pricebook://switchboard/chat/{tier}` and may also record resolved-provider cost. ## SDK unwrap `createProxyFetch` from `@switchboard/sdk` returns provider-native JSON to vendor SDKs on success, and preserves `{ error, data: null, meta }` on failure (never stringifies `null` as the body). ## Non-envelope responses | Surface | Body | Hub facts | |---|---|---| | SSE / streams | Raw provider stream | `SB-*` response headers | | OpenAI-compat `/v1/chat/completions` | Raw OpenAI JSON | `SB-*` headers | ===== FILE: docs/headers.md ===== # Headers ## Request | Header | Required | Purpose | |---|---|---| | `Authorization` | yes | `Bearer sb_…` or OAuth JWT | | `Content-Type` | for JSON bodies | `application/json` | | `Idempotency-Key` | side-effect / hub-idempotent ops | Stable key; reused across cell failover | | `SB-Feature` | no | Product/feature attribution (`onboarding`, `spine`, …) | | `SB-Customer` | no | Customer namespace for BYOK / spend (`atlas-crm:cus_2210`) | | `SB-Region` | no | Preferred region — no silent cross-region fallback | | `SB-Lane` | no | `batch` selects preemptible lane | | `SB-Model` | no | Optional model hint on some proxy paths | ## Response (streams / OpenAI-compat) | Header | Meaning | |---|---| | `SB-Schema` | Envelope schema version (`v1`) | | `SB-Request-Id` | Correlation id | | `SB-Outcome` | `sent` / `failed` / … | | `SB-Cache` | `hit` / `miss` / `bypass` | | `SB-Stream` | `1` when streaming | | `SB-Provider` | Provider account when known | | `SB-Idempotent-Replay` | `1` when hub returned a cached idempotent result | JSON envelope responses also put these facts inside `meta`. ===== FILE: docs/errors.md ===== # Errors > Failures on JSON ops/proxy return an envelope with `data: null` and an `error` object. HTTP status matches the failure class. ## Envelope error body ```json { "data": null, "meta": { "schema": "v1", "request_id": "req_…", "outcome": "failed", "cache": "bypass", "latency_ms": { "hub": 0.3 } }, "error": { "code": "rate_limited", "message": "rate limit exceeded (61/60 in 60s)", "retryable": true, "request_id": "req_…", "retry_after_ms": 1000 } } ``` When `retry_after_ms` is present the response also includes an HTTP `Retry-After` header (seconds, rounded up). Prefer `retry_after_ms` in SDKs; use the header for generic HTTP clients. ## Catalog | Code | HTTP | Retry? | Meaning | What to do | |---|---|---|---|---| | `unauthorized` | 401 | no | Missing/invalid Bearer key | Check `Authorization`; republish LKG after mint/rotate | | `forbidden_scope` | 403 | no | Key lacks connector/op/category scope | Widen scopes; republish LKG | | `forbidden_role` | 403 | no | Key role cannot access this route | Use an app/admin key with the right role | | `forbidden_ip` | 403 | no | Caller IP not on allowlist | Update key IP policy | | `forbidden_plan` | 403 | no | Plan cannot use this thinking tier | Upgrade plan or pick a lower tier (`anybody` / `thinker` / `expert`) | | `forbidden_model` | 403 | no | Model not allowed for this key | Request model allowlist change or pick an allowed model | | `invalid_input` | 400 | no | Bad path/body/unknown op | Fix request; include `Idempotency-Key` when required | | `invalid_request` | 400 | no | Malformed form / request parse | Fix encoding / content-type | | `body_too_large` | 413 | no | Request body over limit | Shrink payload | | `pii_blocked` | 400 | no | Payload failed PII policy | Redact fields or change data policy | | `region_unavailable` | 403 | no | No egress for requested region | Pick a supported `SB-Region` / cell | | `rate_limited` | 429 | yes | RPM/TPM, lane contention, or anomaly throttle | Wait `retry_after_ms` (default 1000); reuse `Idempotency-Key` | | `budget_exceeded` | 402 | no\* | Soft/hard spend cap blocked admit | Raise tenant budget or wait; backoff uses `retry_after_ms` (default 60000) so clients do not hammer | | `insufficient_credits` | 402 | no\* | Prepaid balance empty | Top up credits; same backoff hint as budget | | `dedupe_unavailable` | 503 | yes | Hub idempotency store down | Retry with the **same** `Idempotency-Key` after `retry_after_ms` | | `provider_down` | 502/503 | yes | Upstream/provider failure or empty ladder | Fail over cells; retry with same idempotency key | | `approval_denied` | 403 | no | Operator denied the side-effect tool call | Do not retry; change the action or get approval | | `approval_pending` | 200 (MCP) | poll | Side-effect tool awaiting approve/deny | Poll approval status; do not re-issue a new idempotency key | | `unavailable` | 503 | no | Feature not loaded (e.g. thinking tiers) | Operator must load config; retry later | | `internal` | 500 | sometimes | Unexpected hub error | Retry only if `retryable: true` | \*Not auto-retryable for spend — raising budget/credits is required — but `retry_after_ms` + `Retry-After` are still set so clients back off cleanly. ## Retry rules for agents and SDKs - Honor `error.retry_after_ms` / `Retry-After` on 429, budget, lane, and retryable 5xx. - Retry **connection errors** and **503** across the cell endpoint list; reuse the same `Idempotency-Key`. - Do **not** retry non-idempotent 4xx (`forbidden_scope`, `invalid_input`, `unauthorized`, `pii_blocked`, `forbidden_plan`, `forbidden_model`). - `@switchboard/sdk` `SwitchboardClient` already fails over ordered endpoints on connection/503. ## MCP errors MCP uses JSON-RPC error objects (`code` integer + `message`), not the HTTP envelope, on the MCP response body. HTTP auth failures may still return the envelope. Approval flows surface `approval_pending` / `approval_denied` in the MCP result / error string. ===== FILE: docs/mcp.md ===== # MCP for agents > Switchboard exposes scoped tools over Model Context Protocol JSON-RPC at `POST /v1/mcp`. Same bearer key as HTTP; tool names are `connector.op`. ## Endpoint ```http POST /v1/mcp Authorization: Bearer sb_live_agt_… Content-Type: application/json ``` ## tools/list ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": { "toolset": "ai" } } ``` Alias method: `sb_search_operations`. Optional `toolset` filters (e.g. `ai`, `marketing`) to the key’s MCP toolsets. Only ops with `mcp.expose: true` in connector YAML appear. ## tools/call ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "twilio.send_sms", "arguments": { "To": "+15550192", "Body": "hi" } } } ``` `name` must be `connector.op`. Arguments map to the hub op body. ## Approvals If `mcp.requires_approval` is set on the op, the first call may return an approval-pending result. Operators approve/deny in Control Room (`/v1/approvals`). Denied → JSON-RPC error `approval_denied`. Agent keys are the intended caller kind for MCP traffic. ## Relationship to HTTP | Concern | MCP | HTTP | |---|---|---| | List capabilities | `tools/list` | `GET /v1/connectors` | | Invoke op | `tools/call` name=`connector.op` | `POST /v1/call/connector.op` | | AI chat | tool wrapping proxy/tiers | `/v1/proxy/switchboard/…` | | Auth | same Bearer | same Bearer | Prefer MCP when the runtime is an agent tool loop. Prefer HTTP/SDK when the runtime is a product backend. ===== FILE: docs/sdk-typescript.md ===== # TypeScript SDK > Package: `@switchboard/sdk` — multi-cell failover client + AI SDK proxy helpers. ## Install / import ```bash npm install @switchboard/sdk # or from this monorepo while unpublished publicly: # npm install ./sdk/typescript ``` ```ts import { SwitchboardClient, createProxyFetch, providerProxyBaseUrl, switchboardTiersBaseUrl, THINKING_TIERS, } from "@switchboard/sdk"; ``` Source: `sdk/typescript/` — see package [CHANGELOG.md](../sdk/typescript/CHANGELOG.md). ## Hub-native client ```ts const sb = new SwitchboardClient({ endpoints: [ process.env.SWITCHBOARD_BASE_URL!, // optional failover cell ], apiKey: process.env.SB_KEY!, }); await sb.call({ op: "twilio.send_sms", body: { To: "+15550192", Body: "hi" }, idempotencyKey: "sms-1", feature: "onboarding", }); ``` Retries **connection errors** and **503** across `endpoints`, reusing the same `Idempotency-Key`. Non-retryable 4xx fail immediately. ## AI SDK + thinking tiers ```ts import { createOpenAI } from "@ai-sdk/openai"; import { createAnthropic } from "@ai-sdk/anthropic"; const cell = process.env.SWITCHBOARD_BASE_URL!; const key = process.env.SB_KEY!; const fetch = createProxyFetch(key, { headers: { "SB-Feature": "spine" } }); const tiers = createOpenAI({ apiKey: key, baseURL: switchboardTiersBaseUrl(cell), // /v1/proxy/switchboard fetch, }); // model: "anybody" | "thinker" | "expert" — see THINKING_TIERS const anthropic = createAnthropic({ apiKey: key, baseURL: providerProxyBaseUrl(cell, "anthropic"), fetch, }); const openai = createOpenAI({ apiKey: key, baseURL: providerProxyBaseUrl(cell, "openai"), fetch, }); ``` `createProxyFetch`: - Forces Switchboard Bearer; strips vendor auth headers / Google `?key=` - Unwraps success `{ data, meta }` → provider `data` - Keeps `{ error, data: null }` failures intact - Passes SSE through unchanged ## Env contract ```bash SWITCHBOARD_BASE_URL=https://us1.switchboard.example SB_KEY=sb_live_… # or SWITCHBOARD_API_KEY USE_SWITCHBOARD=true # product flag if applicable ``` ## Types Envelope types can be regenerated from `platform/schemas/envelope.schema.json`: ```bash cd sdk/typescript && npm run gen ``` ===== FILE: docs/agents.md ===== # Guide for coding agents > How an AI coding agent should integrate a product with Switchboard without inventing provider-key wiring. ## Discovery order (follow this) 1. Fetch **`/llms.txt`** (site root) or read `docs/llms.txt` in-repo — curated index, keep it in context. 2. Load **`/openapi.yaml`** (or `docs/openapi.yaml`) for exact paths, params, and schemas. 3. Open only the linked Markdown pages you need (prefer `.md` URLs under `/docs/…`). 4. If you need everything in one shot, use **`/llms-full.txt`** — last resort; it is large. Do **not** scrape marketing HTML for API contracts. Do **not** invent OpenAI-compat as the default multi-provider path. ## Integration checklist 1. Confirm the product will hold **only** `SB_KEY` + `SWITCHBOARD_BASE_URL` (no provider secrets in app env when hub-owned). 2. Pick a mode from [call-modes.md](./call-modes.md): - Product AI → thinking tiers (`anybody` / `thinker` / `expert`) - Existing vendor AI SDKs → proxy + `createProxyFetch` - Payments/SMS/CRM/social → `/v1/call/{connector}.{op}` - Agent runtime → `POST /v1/mcp` 3. Tell the human/operator to mint a key with the right connector scopes and publish LKG (you cannot invent a production key). 4. Wire TypeScript via [sdk-typescript.md](./sdk-typescript.md) when the stack is TS; otherwise raw HTTP per OpenAPI. 5. Always send `Idempotency-Key` on side-effect ops. 6. Parse envelopes correctly: success → use `data`; failure → `error` with `data: null`. 7. For proxy `baseURL`, never double `/v1` — see call-modes table. ## Forbidden patterns - Putting `OPENAI_API_KEY` / `STRIPE_SECRET_KEY` / etc. in the app when Switchboard owns the hop - Pointing Anthropic or Google SDKs at `{cell}/v1/chat/completions` - Setting `baseURL` to `{cell}/v1/proxy/anthropic/v1` (double prefix) - Treating any JSON with `meta` as success without checking `error` - Retrying `403 forbidden_scope` or `401` in a loop - Calling admin (`/v1/admin/…`) with a product caller key ## Base URL Use your cell hostname in production and staging: ```text https://{cell} # e.g. https://us1.switchboard.example ``` Set `SWITCHBOARD_BASE_URL` to that value. Do **not** hard-code `http://127.0.0.1:8080` in product code — that only works when you are running the local gateway. ## Local verification commands From repo `platform/` (when developing against a local cell): ```bash make run-sim # terminal 1 — fake provider make run-gateway # terminal 2 — cell on http://127.0.0.1:8080 # Live smoke: sb_live_app__ # Sandbox (no live spend): sb_test_app_sandbox_devsecret01 export SWITCHBOARD_BASE_URL=http://127.0.0.1:8080 export SB_KEY=sb_test_app_sandbox_devsecret01 ``` Smoke a tier call (set `SWITCHBOARD_BASE_URL` and `SB_KEY` from your cell): ```bash curl -sS "$SWITCHBOARD_BASE_URL/v1/proxy/switchboard/chat/completions" \ -H "Authorization: Bearer $SB_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"anybody","messages":[{"role":"user","content":"ping"}],"max_tokens":16}' ``` ## MCP vs docs MCP - **Switchboard MCP** (`POST /v1/mcp` on a cell) = runtime tools for *product* agents using a Switchboard key. - **These docs** (`llms.txt` + OpenAPI + Markdown) = how *coding* agents should write integration code. They are complementary. A coding agent reads docs; a product agent calls cell MCP. ## Source of truth | Artifact | Role | |---|---| | `docs/openapi.yaml` | HTTP contract for callers | | `docs/*.md` | Human + agent prose | | `platform/schemas/envelope.schema.json` | Envelope JSON Schema | | `platform/connectors/*/connector.yaml` | Per-provider ops (hub-native) | | `sdk/typescript/` | Official TS client | When docs and code disagree, prefer OpenAPI + connector YAML + envelope schema, then file a docs fix. ===== FILE: docs/changelog.md ===== # API changelog Envelope `meta.schema` is currently `"v1"`. Breaking changes require a new schema version and a deprecation window. ## 2026-11 - **Caller self-serve:** `GET /v1/me/billing`, `GET /v1/me/explorer`, webhook list/stats/deliveries/replay under `/v1/me/webhooks/*` - **Public status:** `GET /v1/public/status` (sanitized cell health rollup) - **Test ring:** `sb_test_*` keys route to connector sandbox regions or `SB_SIMULATOR_URL`; Axis billing skipped - **Errors:** `error.retry_after_ms` + HTTP `Retry-After` on 429 / budget / capacity responses - **Alerts:** `ALERT_NOTIFY_URLS` accepts comma-separated destinations (Slack incoming, PagerDuty, email relay, …) ## Policy - Additive OpenAPI fields and new optional headers are non-breaking. - Removing or renaming error codes / paths requires at least 30 days notice on this page and in release notes. - Connector `version` integers change when ops or egress contracts change; cells load the LKG snapshot — callers should not hard-code connector version ints.