openapi: 3.1.0
info:
  title: Switchboard Cell API
  version: "1.0.0"
  summary: Caller-facing HTTP API for Switchboard cells
  description: |
    Switchboard is a hub that owns third-party credentials, egress, metering, and audit.
    Callers authenticate with a Switchboard key (`sb_…`) — never with provider API keys.

    **Preferred integration paths**
    1. Thinking tiers — `POST /v1/proxy/switchboard/chat/completions` with `model: anybody|thinker|expert`
    2. Provider proxy — point vendor SDKs at `/v1/proxy/{connector}`
    3. Hub-native ops — `POST /v1/call/{connector}.{op}`
    4. Agents — `POST /v1/mcp` (JSON-RPC tools/list + tools/call)

    Machine-readable index: `/llms.txt`. Full markdown dump: `/llms-full.txt`.
  contact:
    name: Switchboard
  license:
    name: Proprietary

servers:
  - url: https://{cell}
    description: Production / staged cell — replace {cell} with your hostname (preferred)
    variables:
      cell:
        default: us1.switchboard.example
  - url: http://127.0.0.1:8080
    description: Local gateway only (`make run-gateway`). Never the default for product SDKs or public docs copy-paste.

tags:
  - name: Call
    description: Hub-native named operations from connector.yaml
  - name: Proxy
    description: Transparent provider proxy for vendor SDKs
  - name: Thinking tiers
    description: Switchboard model bands (anybody / thinker / expert)
  - name: MCP
    description: Model Context Protocol JSON-RPC for agents
  - name: OpenAI compat
    description: Raw OpenAI-shaped surface (smoke / single-provider only)
  - name: Discovery
    description: Connector and health discovery
  - name: Auth
    description: OAuth token exchange for customer keys

paths:
  /healthz:
    get:
      operationId: healthz
      tags: [Discovery]
      summary: Liveness probe
      security: []
      responses:
        "200":
          description: Gateway is up
          content:
            text/plain:
              schema:
                type: string
                example: ok

  /oauth/token:
    post:
      operationId: oauthToken
      tags: [Auth]
      summary: Exchange customer client credentials for a short-lived JWT
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type:
                  type: string
                  enum: [client_credentials]
                client_id:
                  type: string
                  example: key_cus_test
                client_secret:
                  type: string
      responses:
        "200":
          description: Access token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthToken"
        "401":
          $ref: "#/components/responses/EnvelopeError"

  /v1/connectors:
    get:
      operationId: listConnectors
      tags: [Discovery]
      summary: List connectors visible to this key
      description: Returns connectors and ops allowed by the caller's scopes.
      responses:
        "200":
          description: Connector catalog slice
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/EnvelopeError"
        "403":
          $ref: "#/components/responses/EnvelopeError"

  /v1/call/{op}:
    post:
      operationId: callOp
      tags: [Call]
      summary: Invoke a hub-native connector operation
      description: |
        `op` is `{connector}.{operation}` from connector.yaml (e.g. `twilio.send_sms`,
        `stripe.create_payment_intent`). Request body is usually the provider's shape
        (or a thin GraphQL/JSON-RPC body). Response is always a Switchboard envelope
        for JSON ops.
      parameters:
        - $ref: "#/components/parameters/OpPath"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/SBFeature"
        - $ref: "#/components/parameters/SBCustomer"
        - $ref: "#/components/parameters/SBRegion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
            examples:
              twilio_sms:
                summary: Send SMS
                value:
                  To: "+15550192"
                  Body: "Your code is 448291"
              stripe_pi:
                summary: Create PaymentIntent
                value:
                  amount: 2500
                  currency: usd
      responses:
        "200":
          description: Envelope with provider payload in data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
              examples:
                twilio:
                  value:
                    data:
                      sid: SM9a2…
                      status: queued
                    meta:
                      schema: v1
                      request_id: req_01J…
                      outcome: sent
                      cache: bypass
                      provider_account: house-primary
                      units:
                        message_segment: 1
                      cost:
                        amount: 0.0075
                        currency: USD
                      price:
                        amount: 0.01
                        currency: USD
                      latency_ms:
                        hub: 2.1
                        provider: 142
        "400":
          $ref: "#/components/responses/EnvelopeError"
        "401":
          $ref: "#/components/responses/EnvelopeError"
        "403":
          $ref: "#/components/responses/EnvelopeError"
        "429":
          $ref: "#/components/responses/EnvelopeError"
        "503":
          $ref: "#/components/responses/EnvelopeError"

  /v1/proxy/{connector}/{path}:
    parameters:
      - $ref: "#/components/parameters/ConnectorPath"
      - name: path
        in: path
        required: true
        description: Provider-relative path (as the vendor SDK would append to its baseURL)
        schema:
          type: string
        examples:
          chat: chat/completions
          messages: messages
          embeddings: embeddings
      - $ref: "#/components/parameters/SBFeature"
      - $ref: "#/components/parameters/SBCustomer"
      - $ref: "#/components/parameters/SBRegion"
      - $ref: "#/components/parameters/IdempotencyKey"
    get:
      operationId: proxyGet
      tags: [Proxy]
      summary: Proxy GET to a provider via Switchboard
      responses:
        "200":
          description: Envelope (JSON) or raw stream
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/EnvelopeError"
        "403":
          $ref: "#/components/responses/EnvelopeError"
    post:
      operationId: proxyPost
      tags: [Proxy, Thinking tiers]
      summary: Proxy POST to a provider (or Switchboard thinking tiers)
      description: |
        Point vendor SDK `baseURL` at `{cell}/v1/proxy/{connector}` — do **not** double
        the version prefix (Anthropic/Google already include `/v1` or `/v1beta` in their
        connector base).

        For thinking tiers use connector `switchboard` and `model: anybody|thinker|expert`.
        JSON responses are enveloped; SSE streams are raw with `SB-*` headers.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
            examples:
              thinker:
                summary: Thinking tier chat
                value:
                  model: thinker
                  messages:
                    - role: user
                      content: Draft a refund email
                  max_tokens: 200
              openai_chat:
                summary: OpenAI chat via proxy
                value:
                  model: gpt-4.1-mini
                  messages:
                    - role: user
                      content: Summarize this ticket
      responses:
        "200":
          description: Envelope or SSE
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
            text/event-stream:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/EnvelopeError"
        "403":
          $ref: "#/components/responses/EnvelopeError"
    put:
      operationId: proxyPut
      tags: [Proxy]
      summary: Proxy PUT
      responses:
        "200":
          description: Envelope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
    patch:
      operationId: proxyPatch
      tags: [Proxy]
      summary: Proxy PATCH
      responses:
        "200":
          description: Envelope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
    delete:
      operationId: proxyDelete
      tags: [Proxy]
      summary: Proxy DELETE
      responses:
        "200":
          description: Envelope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /v1/public/status:
    get:
      operationId: publicStatus
      tags: [Discovery]
      summary: Public cell health rollup (no auth)
      description: Sanitized status for status pages — no connector roster or secrets.
      security: []
      responses:
        "200":
          description: Envelope with ok, cells[], optional snapshot_age_s
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "503":
          description: One or more cells unhealthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /v1/me/billing:
    get:
      operationId: meBilling
      tags: [Discovery]
      summary: Caller-scoped usage and spend for the authenticated key
      description: |
        Tenant self-serve rollup for the bearer key (`app` / `agt` / `cus`).
        Operators use admin billing; callers use this (or the marketing `/portal` page).
      parameters:
        - name: period
          in: query
          schema:
            type: string
            example: "2026-11"
          description: Billing period `YYYY-MM` (defaults to current UTC month)
      responses:
        "200":
          description: Envelope with period, caller_id, key_id, and report
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/EnvelopeError"

  /v1/me/explorer:
    get:
      operationId: meExplorer
      tags: [Discovery]
      summary: Caller-scoped ledger explorer
      description: |
        Search / tail the usage ledger filtered to the authenticated caller_id / key_id.
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: Optional search; defaults to the caller's id
      responses:
        "200":
          description: Envelope with hits[]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "401":
          $ref: "#/components/responses/EnvelopeError"

  /v1/me/webhooks/subscriptions:
    get:
      operationId: meListWebhookSubscriptions
      tags: [Discovery]
      summary: List webhook subscriptions owned by this caller
      responses:
        "200":
          description: Envelope with redacted subscription list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
    post:
      operationId: meCreateWebhookSubscription
      tags: [Discovery]
      summary: Create a webhook subscription (secret returned once)
      responses:
        "200":
          description: Envelope with subscription including secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /v1/me/webhooks/stats:
    get:
      operationId: meWebhookStats
      tags: [Discovery]
      summary: Delivery stats for this caller's subscriptions
      responses:
        "200":
          description: Envelope with counts
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /v1/me/webhooks/deliveries:
    get:
      operationId: meWebhookDeliveries
      tags: [Discovery]
      summary: Recent deliveries / DLQ for this caller
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, delivered, failed, dlq]
      responses:
        "200":
          description: Envelope with deliveries[]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"

  /v1/me/webhooks/subscriptions/{id}/replay:
    post:
      operationId: meReplayWebhookSubscription
      tags: [Discovery]
      summary: Replay events for an owned subscription
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Envelope with enqueued count
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Envelope"
        "403":
          $ref: "#/components/responses/EnvelopeError"

  /v1/mcp:
    post:
      operationId: mcpJsonRpc
      tags: [MCP]
      summary: MCP JSON-RPC endpoint for agents
      description: |
        Supports `tools/list` (alias `sb_search_operations`) and `tools/call`.
        Tool names are `connector.op`. Optional `toolset` filter (e.g. `ai`, `marketing`).
        Side-effect tools may require approval before execution.
      parameters:
        - $ref: "#/components/parameters/SBFeature"
        - $ref: "#/components/parameters/SBCustomer"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MCPRequest"
            examples:
              list:
                summary: List tools
                value:
                  jsonrpc: "2.0"
                  id: 1
                  method: tools/list
                  params:
                    toolset: ai
              call:
                summary: Call a tool
                value:
                  jsonrpc: "2.0"
                  id: 2
                  method: tools/call
                  params:
                    name: twilio.send_sms
                    arguments:
                      To: "+15550192"
                      Body: "hi"
      responses:
        "200":
          description: JSON-RPC result or error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MCPResponse"
        "401":
          $ref: "#/components/responses/EnvelopeError"
        "403":
          $ref: "#/components/responses/EnvelopeError"

  /v1/chat/completions:
    post:
      operationId: openaiCompatChat
      tags: [OpenAI compat]
      summary: OpenAI-compatible chat (raw JSON)
      description: |
        Returns **raw** OpenAI-shaped JSON. Hub facts ride on `SB-*` response headers.
        Pinned to the gateway `-llm` connector — prefer `/v1/proxy/switchboard` or
        `/v1/proxy/{provider}` for multi-provider products.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Raw OpenAI chat completion
          headers:
            SB-Request-Id:
              schema:
                type: string
            SB-Outcome:
              schema:
                type: string
            SB-Cache:
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v1/embeddings:
    post:
      operationId: openaiCompatEmbeddings
      tags: [OpenAI compat]
      summary: OpenAI-compatible embeddings (raw JSON)
      description: Prefer `POST /v1/proxy/openai/embeddings` so you are not coupled to `-llm`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Raw OpenAI embeddings response
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

components:
  securitySchemes:
    SwitchboardBearer:
      type: http
      scheme: bearer
      bearerFormat: sb_live_… or JWT
      description: |
        `Authorization: Bearer sb_…` from Control Room / control plane, or a JWT from
        `POST /oauth/token` for customer client-credentials keys.

  parameters:
    OpPath:
      name: op
      in: path
      required: true
      description: Hub operation id — `connector.operation`
      schema:
        type: string
        pattern: "^[a-z0-9_]+\\.[a-z0-9_]+$"
        example: twilio.send_sms
    ConnectorPath:
      name: connector
      in: path
      required: true
      description: Connector id (openai, anthropic, switchboard, stripe, …)
      schema:
        type: string
        example: switchboard
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Required for side-effect / hub-idempotent ops. Replayed on 503 failover.
      schema:
        type: string
    SBFeature:
      name: SB-Feature
      in: header
      required: false
      description: Product/feature attribution tag for metering
      schema:
        type: string
        example: onboarding
    SBCustomer:
      name: SB-Customer
      in: header
      required: false
      description: Customer namespace for BYOK / spend attribution
      schema:
        type: string
        example: atlas-crm:cus_2210
    SBRegion:
      name: SB-Region
      in: header
      required: false
      description: Preferred cell region (no silent cross-region fallback)
      schema:
        type: string
        example: us1

  responses:
    EnvelopeError:
      description: Switchboard envelope with error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Envelope"
          example:
            data: null
            meta:
              schema: v1
              request_id: req_01M…
              outcome: failed
              cache: bypass
              latency_ms:
                hub: 0.3
            error:
              code: forbidden_scope
              message: key lacks scope for operation
              retryable: false
              request_id: req_01M…

  schemas:
    Money:
      type: object
      properties:
        amount:
          type: number
        currency:
          type: string
          example: USD
    Latency:
      type: object
      required: [hub]
      properties:
        hub:
          type: number
        injector:
          type: number
        provider:
          type: number
    EnvelopeMeta:
      type: object
      required: [schema, request_id, outcome, cache, latency_ms]
      properties:
        schema:
          type: string
          const: v1
        request_id:
          type: string
        trace_id:
          type: string
        provider_account:
          type: string
          description: house-primary, byok:caller:…, etc.
        region:
          type: string
        outcome:
          type: string
          enum: [sent, failed, unknown, pending]
        cache:
          type: string
          enum: [hit, miss, bypass]
        model:
          type: string
        resolved_connector:
          type: string
          description: Present on thinking-tier calls
        resolved_model:
          type: string
          description: Present on thinking-tier calls
        units:
          type: object
          additionalProperties:
            type: number
        cost:
          $ref: "#/components/schemas/Money"
          description: Provider cost from the price book
        price:
          $ref: "#/components/schemas/Money"
          description: Bill-to-caller price from the price book
        saved:
          $ref: "#/components/schemas/Money"
        latency_ms:
          $ref: "#/components/schemas/Latency"
        obligations:
          type: array
          items:
            type: string
    EnvelopeErrorBody:
      type: object
      properties:
        code:
          type: string
          examples:
            - forbidden_scope
            - invalid_input
            - dedupe_unavailable
            - rate_limited
        message:
          type: string
        retryable:
          type: boolean
        request_id:
          type: string
        retry_after_ms:
          type: integer
          minimum: 0
          description: Backoff hint; mirrored as HTTP Retry-After (seconds)
    Envelope:
      type: object
      required: [data, meta]
      description: |
        JSON ops and proxy return this wrapper. `data` is the third-party payload.
        On errors `data` is null and `error` is set — do not treat presence of meta as success.
      properties:
        data:
          nullable: true
          description: Third-party JSON (or null on error)
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
        error:
          $ref: "#/components/schemas/EnvelopeErrorBody"
    OAuthToken:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 900
    MCPRequest:
      type: object
      required: [jsonrpc, method]
      properties:
        jsonrpc:
          type: string
          const: "2.0"
        id:
          description: Request id
        method:
          type: string
          enum: [tools/list, tools/call, sb_search_operations]
        params:
          type: object
          additionalProperties: true
    MCPResponse:
      type: object
      properties:
        jsonrpc:
          type: string
          const: "2.0"
        id: {}
        result:
          description: Method result
        error:
          type: object
          properties:
            code:
              type: integer
            message:
              type: string

security:
  - SwitchboardBearer: []
