> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Mock any vendor API

> Describe a third-party API your agent calls, capture what it really returns, seed a world from it through the schema, and let the world serve the vendor's routes so the agent runs against it unchanged.

A world can stand in for any third-party service your agent calls — a breach-data provider, a
CRM, an intelligence feed. Describe the connection once, capture what the real service returns,
seed the world from those captures through its schema, and serve the vendor's own routes from
the world. Your agent's client points at the mock without a code change.

Everything below runs with the `gateway` CLI from the `@withgateway/sdk` npm package. Nothing
is provisioned on your side; publishing and hosting use the platform.

## 1. Start a world and describe the connection

```bash theme={null}
gateway worlds schema init ./vendor-world --connector custom --name vendor-world
```

Edit `./vendor-world/schema/world.json` so its entities match the records the vendor returns,
then write `./vendor-world/connector.toml`:

```toml theme={null}
[connector]
slug = "vendor"
base_url = "https://api.vendor.example/v2"

[auth]
scheme = "header"            # header | bearer | basic | query | oauth2 | none
name = "x-api-key"
secret_env = "VENDOR_API_KEY"   # the key is only ever named, never written here

[capture]
requests_per_minute = 30     # be sparing with the real service
max_pages = 5
cursor_param = "cursor"
cursor_field = "cursor"

[redact]
hash = ["email", "password"] # hashed before anything reaches disk
drop = ["ssn"]
# preserve and round are the other two modes

[api]
envelope = "results"         # rows come back as {results, hits, cursor}
unauthorized = '{"message": "Invalid API key"}'

[[operations]]
name = "get_records_by_email"
path = "/records/by-email/{email}"
params = ["since"]
entity = "records"
results = "results"
[operations.api]
filter = { email = "attributes.email" }
```

List only the operations your agent actually calls. `gateway worlds connector validate
./vendor-world` checks the file and lists them. [The files a world is made of](/worlds/contract)
documents every table and default in `connector.toml` and `schema/world.json`.

The `[redact]` block decides what a captured value becomes before it reaches disk. `hash` and
`drop` are shown above; `preserve` and `round` are the other two modes, and the salt is
`[connector] slug`. See [Keep real data out](/worlds/redaction).

## 2. Capture and seed

```bash theme={null}
export VENDOR_API_KEY=...
gateway worlds connector capture ./vendor-world --op get_records_by_email --arg email=probe@example.com
gateway worlds connector ingest ./vendor-world
```

`capture` follows the vendor's cursor, writes every page under `captures/` with a manifest,
records the request without the secret, redacts the body per `[redact]`, and keeps error
responses, because the vendor's error bodies are part of what the mock must reproduce.
`ingest` turns the successful captures into the world's starting state, checking the rows
against the schema in a scratch copy before anything is written. Re-running `ingest` with no
new captures writes nothing.

A local capture calls the vendor from your machine and needs `python3 >= 3.12` on the PATH.
Without one, capture with `--remote` below.

<Info>
  Captures hold real records even after redaction. Keep `captures/` out of anything you share;
  the world's schema and `connector.toml` are what travel.
</Info>

Rows you already have — a fixture file, a sanitized export, a previous run — go in with
`gateway worlds data import` instead; see [Put data in a world](/worlds/data).

## 3. Serve the vendor's API

```bash theme={null}
gateway worlds serve ./vendor-world --port 8080
curl -H 'x-api-key: conform' 'http://localhost:8080/records/by-email/probe%40example.com'
```

The world answers the routes in `connector.toml` from its store, in the vendor's
envelope, with the vendor's auth scheme and error bodies. Locally it accepts the credential
`--api-key` pins, `conform` when you pass none. Point your agent's client at it
locally, or publish it:

```bash theme={null}
gateway bench push ./vendor-world                                   # publishes the tree as it is
gateway worlds session open vendor-world -t default --surface api   # prints the hosted api.url
```

A hosted session accepts its own api token, as a bearer or in the vendor's own header;
`session open` prints it beside `api.url`.

## 4. Writes and workflows

Lookup vendors are served from the store alone. Workflow vendors need more: the client creates
a search, sets criteria, executes it, polls a status and reads results. The same `connector.toml`
covers that:

```toml theme={null}
[[operations]]
name = "create_search"
method = "POST"
path = "/searches"
entity = "vendor_searches"
[operations.api]
action = "create"                              # read | create | update | delete | handler
status = 201
body = { name = "name", description = "description" }
defaults = { status = "drafted" }
generate = { id = "uuid", createdAt = "now" }

[[operations]]
name = "execute_search"
method = "POST"
path = "/searches/{id}/execute"
entity = "vendor_searches"
[operations.api]
action = "handler"
handler = "execute_search"                     # a function in the world's api/handlers.py
```

Writes persist through the world's contract: a body the vendor would reject gets the vendor's
400, and the next read sees the write. A handler is plain Python with the store and a clock:

```python theme={null}
LADDER = [(0, "queued"), (5, "started"), (20, "completed")]

def execute_search(ctx, args, query, body):
    search = ctx.get("vendor_searches", {"id": args["id"]})
    if search is None:
        return ctx.not_found()
    ctx.update("vendor_searches", {"id": args["id"]}, {"executedAt": ctx.now()})
    return 202, {"jobId": args["id"], "status": "submitted"}

def search_status(ctx, args, query, body):
    search = ctx.get("vendor_searches", {"id": args["id"]})
    return {"status": ctx.ladder(search.get("executedAt"), LADDER, before="drafted")}
```

`ctx` reads and writes (`rows`, `get`, `find`, `insert`, `update`, `delete`), keeps time (`now`,
`elapsed`, `ladder`; set `GATEWAY_WORLD_CLOCK` to an epoch to freeze it), mints ids (`uuid`) and
shapes answers (`envelope`, `not_found`, `invalid`). A handler may answer `(status, body, headers)`, and
`[api] before = "gate"` names a function that runs ahead of every route to apply the vendor's per-key
rules (quotas, allow-lists) to plain reads and writes as well. Handlers ship inside the pushed world, so
the hosted container runs them as-is.

Vendors that authenticate with OAuth2 client credentials declare `scheme = "oauth2"` with
`token_url`, `client_id_env` and `scope`: capture exchanges the credentials for a bearer before
its first request, and the hosted world mints bearers on the same token path, so a client that
performs the grant first works unchanged. Pagination follows the vendor too: `[api] page` is
`none`, `offset` or `cursor` with the vendor's own parameter and envelope key names, and a
per-route `single = true` answers the object itself rather than a list.

[The files a world is made of](/worlds/contract) lists every key: the `[api]` envelope, paging
and error family, every `[operations.api]` key with its default, `[capture]`,
`[operations.ingest]`, and `[ui]` for shipping a dashboard with the world.

## 4b. Capture from the platform's IP

Some vendors allow-list the caller's address. Store the credential on the environment once and
let the platform make the calls:

```bash theme={null}
gateway bench secrets set --env vendor-world --key VENDOR_API_KEY
gateway worlds connector capture ./vendor-world --op get_records_by_email --arg email=x@y.com --remote
```

The pages come back redacted into `captures/` exactly as a local capture would write them, so
`connector ingest` is unchanged. Give the vendor the platform's egress address when they ask for
one; the credential is held encrypted on the project and never leaves the platform.

## 5. Prove it against the vendor's OpenAPI

```bash theme={null}
gateway worlds connector conform ./vendor-world --openapi vendor-openapi.yaml --out report.json
```

Every declared operation is called on the served world with arguments taken from its own seed,
and each answer is checked against the vendor's document: the documented status and a body the
response schema accepts. The exit code is non-zero on a mismatch, so it runs in CI. Scenarios
(`scenarios/<name>/` with seed overlays and an instruction) let a session start the world in a
given state — `gateway worlds serve --scenario quota-exhausted` locally, or any of them hosted.

Work through the failures with `gateway worlds schema reconcile ./vendor-world`. It compares
`schema/world.json` against what the vendor really returned in `captures/`, then relaxes
required fields the captures omit, adds undocumented keys as optional properties, and drops
formats the captured values do not meet. Changes are printed and nothing is written until
`--write`; `--from` names a source other than `captures`.

## 6. Save it as a workspace connector

```bash theme={null}
gateway worlds connector publish ./vendor-world --slug vendor -m "first cut"
gateway worlds connector list --hub
gateway worlds schema init ./vendor-world-2 --connector workspace:vendor
```

`publish` saves the template — the contract, the connection, the handlers, scenarios and
reports, never `captures/` or `data/` — on the platform as a workspace connector: versioned,
owned by your organization, with its own page. The platform compiles it first and refuses a
template that does not make a world, telling you why. Publish again after a change and it
becomes a new version. Anyone in your organization starts a fresh world from it with
`--connector workspace:<slug>`, and `gateway worlds connector pull <slug>` writes the files back
out.

Connectors another organization published show up in `list --hub`; `gateway worlds connector
clone <slug>` copies one into your organization with its origin recorded. The publisher's later
changes do not reach the copy unless you clone again.

For an agent doing the work, `gateway worlds connector skill --install .` puts the authoring
checklist and the `connector-template-author` agent into the repository's `.claude/`, so Claude
Code or Cursor builds the world with the steps above and finishes with `publish`.

## When the vendor's shape does not fit the schema

* A vendor that inlines a collection on a row (users carrying `channels[]`) when your schema
  keeps it as its own entity: add `[operations.ingest] drop = ["channels"]` and an
  `[[operations.ingest.explode]]` (`field`, `entity`, `carry`, `take`) so each element becomes
  a child row.
* Credentials in several headers: `[auth] headers = { "X-App-Token" = "VENDOR_APP_TOKEN" }`.
* Lookups driven by query parameters rather than the path: name the parameter in
  `[operations.api] filter`; several operations may share one path and are told apart by the
  parameters sent.
* A vendor that answers 403 rather than 401: `[api] unauthorized_status = 403`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.