# Switchboard

Switchboard turns an external JSON or XML HTTP API into MCP tools. You describe one API operation
as a **mapping definition**: a static request shape (method, base URL, path) plus small
[CEL](/docs/expressions.md) expressions that fill in values and prune the response. Switchboard
validates the definition, stores every attempt as an immutable revision, and serves a mapping's
last-good definition as a tool on its MCP endpoint.

No code you write is executed. Expressions are pure data transforms: they cannot reach the
network, files or the credential. The host builds and sends the HTTP request, injects the API
key, and wraps your result in the MCP envelope.

These pages are written for an AI agent that authors definitions. Read this page, then
[definitions](/docs/definitions.md) and [expressions](/docs/expressions.md), then copy the worked
example closest to your target API.

## Plans

Every environment is on one of two plans. **Free** allows 30 transformation requests per UTC
day; **Paid** ($10 a month) is unlimited. A transformation request is a tool call of one of your
mappings, or a webhook Switchboard receives and stores. Nothing else counts. Once Free's 30 are
used, a tool call fails with [`daily_limit_exceeded`](/docs/errors.md) and a webhook is answered
`429` with a `retry-after` until midnight UTC. `GET /billing` shows your plan and today's count.
A person upgrades or cancels after logging in to the portal at `/portal` (sign up at
`/users/register`; during launch the portal is invite-only, and anyone not yet invited waits on a
waiting list). An environment appears in someone's portal when they open its registration
link while logged in, or when its registration email matches their account's.

## Pages

- [Authentication](/docs/authentication.md): agents connect through EndPointBlank (sign in there, no secret), or the client id and secret for scripts
- [Registration](/docs/registration.md): the one-time step every EndPointBlank environment needs before any authorized route works
- [Mapping definitions](/docs/definitions.md): every field, every rule, the XML convention
- [Expressions](/docs/expressions.md): bindings, optional values, lists, numbers, null means omit
- [Validation](/docs/validation.md): error codes and the correction loop
- [Runtime errors](/docs/errors.md): the four error categories and `retryable`
- [Credentials](/docs/credentials.md): API keys, `auth_ref`, rotation
- [Webhooks](/docs/webhooks.md): signature verification, filtering and mapping a vendor's payload, deliveries, replay-dedup
- [Events](/docs/events.md): reading stored events and deliveries, reprocessing and redelivering
- [Targets](/docs/targets.md): the managed agents a webhook delivers to
- [Activity](/docs/activity.md): a live view of everything Switchboard has recorded for a tenant
- [Definition JSON Schema](/schema/mapping-definition.json): generated from the validator's own source
- [Webhook definition JSON Schema](/schema/webhook-definition.json): generated from the webhook validator's own source

## Worked examples

Every example below is checked by Switchboard's test suite and validates as `active`.

| Example | What it shows |
|---|---|
| [json_get_pruning.json](/docs/json_get_pruning.json) | JSON GET; optional query parameters; null means omit; filtering and pruning a large response |
| [json_post_body.json](/docs/json_post_body.json) | JSON POST body built from arguments; defaults; `now`; an optional header |
| [xml_get_response.json](/docs/xml_get_response.json) | XML response; attributes, text and always-list children; string-to-number conversion |
| [xml_post_body.json](/docs/xml_post_body.json) | XML request body with `xml_root` and an attribute on the root |
| [form_post.json](/docs/form_post.json) | `application/x-www-form-urlencoded` body; including a field only when an argument is present |
| [error_mapping.json](/docs/error_mapping.json) | path parameter; `error_mapping` that extracts the vendor's error code, message and request id |

Mappings are one direction: an agent calling out to an API. If you need the other direction — a
vendor calling *in* — see [webhooks](/docs/webhooks.md): turning a signed HTTP callback into a
turn a [target](/docs/targets.md) agent receives, with its own worked examples.

## How authoring works

1. **Authenticate, and register once.** Every request to Switchboard carries EndPointBlank
   credentials in the `Authorization` header: a Bearer token an agent gets by
   [signing in through EndPointBlank](/docs/authentication.md) (preferred for agents), or the
   client id and secret as HTTP Basic. Your tenant is the EndPointBlank application
   environment those credentials belong to. There is no tenant field anywhere; never invent one.
   Before any of this works, someone has to complete the one-time [registration](/docs/registration.md)
   flow for that environment — every **authorized** route except `POST /registrations` refuses an
   unregistered one with `403 environment_not_registered`.
2. **Store the vendor's API key once**, if the API needs one: `POST /credentials`. The secret is
   write-only. Reference it from a definition by its id in `auth_ref`. See
   [credentials](/docs/credentials.md).
3. **Create the mapping:** `POST /mappings` with the definition. When a mapping row can be stored
   the answer is `201`, whether or not the definition is valid. Read `data.status` and
   `data.current_revision.validation.errors`.
4. **Correct it:** `PATCH /mappings/name:<name>` with the *complete* corrected definition, not a
   diff. Repeat until `data.status` is `active`. See [validation](/docs/validation.md).
5. **Use it.** Agents connected to `POST /api/mcp` see one tool per mapping that has ever had a valid
   revision, named after the mapping. A mapping is served from the moment its first valid revision
   is written — it does not need to be valid *right now*: a later invalid `PATCH` leaves the last
   good revision serving under its own content, and the mapping shows `status: "pending"` (not
   `"active"`) until a valid write replaces it. A mapping that has never had a valid revision is
   never served. See ["what gets served" on the validation page](/docs/validation.md#what-gets-served).

Send the same `x-session-id` header on every write in one authoring session, so the attempts can
be reviewed together later with `GET /revisions?session_id=<id>`.

## Quick start

```bash
curl -sS -X POST https://switchboard.example.com/mappings \
  -H "Authorization: Basic <base64 of client_id:client_secret>" \
  -H 'content-type: application/json' \
  -H 'x-session-id: 7d1c2f0e-weather-attempt' \
  --data @definition.json
```

```json
{
  "data": {
    "id": "0b6f7a52-3c1e-4d6a-9d3e-2f7b8c9a1e44",
    "name": "search_repositories",
    "status": "pending",
    "active_revision_id": null,
    "current_revision": {
      "revision_number": 1,
      "status": "pending",
      "validation": {
        "checked_at": "2026-09-12T10:00:00.000000Z",
        "errors": [
          {
            "path": "request.query.q",
            "code": "unresolved_reference",
            "message": "references `args.querry`, which is not a property in input_schema",
            "expected_one_of": ["include_archived", "language", "order", "per_page", "query"]
          }
        ],
        "dry_run": null
      }
    }
  }
}
```

The response is abridged; the full revision shape is on the [validation](/docs/validation.md) page.

## Rules worth memorising

- A single mapping or credential is always addressed as `id:<uuid>` or `name:<name>`, for example
  `/mappings/name:search_repositories`. An unqualified segment is `400 unqualified_selector`.
- Names match `^[a-z][a-z0-9_]{0,63}$`.
- `method`, `base_url` and the shape of `path` are static text. Expressions fill in values only.
- An expression that evaluates to `null` omits that query parameter or header.
- In XML, child elements are **always lists**, even when there is only one.
- A response mapping returns plain data. Never build `content`, `isError` or any MCP envelope.
- A mapping is served on the strength of its **last good** revision, not its latest write; see
  [validation](/docs/validation.md#what-gets-served).
