> ## 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.

# Core Resources

> Route tables and worked curl examples for the Surface Area REST API, covering worlds, world sessions, stored tasks, traces, sessions, observations, scores, datasets, annotation queues, media, metrics, and models.

Every route below lives under `/api/public` on your Surface Area host and authenticates with your project key pair over HTTP Basic auth. The examples assume `GATEWAY_HOST`, `GATEWAY_PUBLIC_KEY`, and `GATEWAY_SECRET_KEY` are exported, as shown on the [Overview](/rest-api) page.

Paths in the tables omit the `/api/public` prefix for readability. A path of `/traces` is `$GATEWAY_HOST/api/public/traces`.

## Worlds

A [world](/worlds) is created from a connector template, a published workspace connector, or a tree of files sent as the body. The same request backs `gateway worlds create` and the MCP tool `create_world`.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/worlds` | Create a world with `{ slug, from?, overlay?, data?, name?, description?, message? }`. Answers `201` with the world, its first `READY` version, `url` (the world's page in the app), `links` (`api`: the JSON descriptor at `/world/<slug>`; `world`, `tasks`, `sessions`); `400` names what the contract refused, `409` that the slug is taken. Secret key required. |
| `GET` | `/worlds/{slug}` | The world as a URL: its head `READY` version, surfaces, and files. Here `url` is the JSON descriptor itself (`/world/<slug>`), the value `POST /worlds` answers as `links.api`. |
| `GET` | `/worlds/templates` | The connector templates this instance installs. |
| `GET` | `/worlds/{slug}/tasks` | The world's tasks at its head version. |
| `GET` | `/worlds/{slug}/openapi.json` | The OpenAPI document the world's API answers to. |
| `POST` | `/worlds/runtime` | Run one schema-runtime command over a tree of files sent in the body. Secret key required. |

### How a world is stored

The platform stores the `gateway-world/1` tree as it is, and one session serves both the world's tools and its HTTP routes.

A world's descriptor carries two scriptable blocks. `redact` is the connector's `[redact]` table — `slug` (the digest salt, which is the connector's own slug and can differ from the platform slug), `hash`, `drop`, `preserve` and `round` — or `null` when the world declares none. `dependencies` is the head version's resolved `[dependencies]` links, each with its `alias`, the version it depends on, its `task` and `tools`, and the `entities` mappings that carry a field across. See [Link worlds together](/worlds/links) for what a link does and [Keep real data out](/worlds/redaction) for what each redaction mode means.

### Reach the schema runtime without Python

The world compiler, the store and `describe` are Python and run on the platform. `POST /worlds/runtime` reaches them over HTTP: the body is the runtime request verbatim — `{ "command": "...", "files": [{ "path": "...", "content": "<base64>" }] }` — and the answer is the runtime's own reply, `{"status": "ok", ...}` or a `400` carrying `{"status": "rejected", "error": {code, message, path}}` that names the file and field it refused.

Allowed commands include `validate`, `compile`, `init-template`, `init-template-files`, `template-summary`, `describe`, `import-data`, `ingest`, `conform`, `counts` and `templates`.

<Info>
  The `gateway` CLI runs the same code on your machine when it finds
  `python3` 3.12 or newer, and posts it here when it does not. A
  `gateway worlds schema check` on a machine without Python is this route.
</Info>

<Info>
  List worlds with `GET /environments`. The world detail and version
  routes under `/environments/{containerId}` are documented on the [Worlds
  client](/sdk/environments#rest-api) page.
</Info>

<Info>
  The world, world-session and world-task routes read with either key and
  write with one. A `GET` accepts `Authorization: Bearer $GATEWAY_PUBLIC_KEY`
  as well as Basic auth; every `POST`, `PATCH` and `DELETE` on them requires
  the secret key over Basic and answers `403` to a public key. The one
  exception is `POST /world-tasks/{taskId}/validate`, which writes nothing
  and takes either.
</Info>

### Put data in a world

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/worlds/{slug}/data` | What the head version seeds with: per-entity row counts, or one entity's rows with `?entity=`. |
| `POST` | `/worlds/{slug}/data/import` | Rows in as a batch, validated against the contract. Answers the batch id to poll; a refused row names its field and changes nothing. |
| `GET` | `/worlds/{slug}/data/batches` | The world's data batches, newest first. Page with `limit` (max 100) and `cursor`. |
| `POST` | `/worlds/{slug}/data/batches` | Open a chunked batch for a large upload. |
| `GET` | `/worlds/{slug}/data/batches/{batchId}` | One batch's state, counts and first refusals. |
| `POST` | `/worlds/{slug}/data/batches/{batchId}/chunks` | Register one gzipped chunk by digest and get a presigned upload URL. |
| `POST` | `/worlds/{slug}/data/batches/{batchId}/chunks/{chunkId}/uploaded` | Confirm the upload. The server checks the stored object, not the client's claim. |
| `POST` | `/worlds/{slug}/data/batches/{batchId}/complete` | Seal the batch and hand it to the import job. Answers `202`. |
| `POST` | `/worlds/{slug}/data/publish` | Fold every applied deferred batch into one data-only version. |

### Read rows back

Without `entity`, `GET /worlds/{slug}/data` answers per-entity row counts. Name an entity and the other parameters open up.

| Parameter | Default | What it does |
| - | - | - |
| `entity` | — | The entity whose rows come back. Every parameter below needs it. |
| `where` | — | A JSON object of field to value, or field to a list of values, which matches any of them. |
| `limit` | `100` | Rows in the page, at most `1000`. |
| `offset` | `0` | Where the page starts. Walk it for the rest. |
| `resolve` | `false` | `true` reads each `where` value as plaintext and maps it through the world's `[redact]` table before matching, so a hashed field is found from what it held. |
| `across` | `false` | `true` runs the same query in every linked world whose entity mappings carry a `where` field across. The answer becomes `groups`, the root world first, instead of one page. |
| `compare` | `false` | With `across`, adds the parity report `compare`. |

Asking for `across` or `compare` without an `entity` answers `400`.

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/worlds/acme-crm/data" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "entity=accounts" \
  --data-urlencode 'where={"owner_email":"jo@acme.test"}' \
  --data-urlencode "resolve=true"
```

### Send rows inline

`POST /worlds/{slug}/data/import` takes `{ rows, mode?, redact?, message?, dryRun? }`, where `rows` is `{"<entity>": [row, ...]}`. A large file goes through a batch. `dryRun` validates every row and reports without writing.

The route follows the batch for 55 seconds. It answers `201` with the version the rows became, or `202` with `{batchId, state, rowCount}` when the batch outlasted the wait — poll `GET /worlds/{slug}/data/batches/{batchId}` from there.

`redact` chooses what happens to plaintext on the way in: `off` (the default), `apply`, or `refuse`. See [Keep real data out](/worlds/redaction) for what each mode does.

### Upload a large file as a batch

`POST /worlds/{slug}/data/batches` takes `{ mode?, merge?, redact?, atomic?, dryRun?, publish?, message?, expectedChunks?, entities? }` and answers `201` with `{ batchId, state: "open", limits }`. The `limits` block holds the sizes to chunk against: `chunkBytes` (per chunk, compressed), `batchBytes` (per batch, compressed) and `inlineBytes` (what the import route takes inline). `redact` takes the same three values as on import.

The sequence is five calls:

1. **Open the batch** — `POST /worlds/{slug}/data/batches`. Keep the `batchId` and the `limits`.
2. **Register each chunk** — `POST …/batches/{batchId}/chunks` with `{ ordinal, digest, bytes, rows, entity? }`, where `digest` is the hex sha256 of the gzipped bytes and `bytes` is their gzipped size. The answer carries `uploadUrl`, the `uploadHeaders` the URL was signed for, and `expiresAt` an hour out.
3. **Upload the bytes** — `PUT` the gzipped chunk to `uploadUrl`, sending `uploadHeaders` exactly as given.
4. **Confirm the upload** — `POST …/chunks/{chunkId}/uploaded`. The server stats the stored object and refuses when its size differs from what was registered.
5. **Seal the batch** — `POST …/complete` answers `202` and state `queued`. Poll `GET …/batches/{batchId}` until the state settles.

Registering the same ordinal with the same digest again returns the same chunk, so an interrupted upload resumes. A different digest replaces the chunk while it is unuploaded and is refused once it is not. Bytes the world already holds under that digest, in any of its batches, come back `uploaded: true` with no URL, so nothing is uploaded twice.

`complete` refuses while any registered chunk is unconfirmed, naming the ordinals, and refuses when the chunk count differs from an `expectedChunks` the batch declared. Completing a batch that is already `queued` changes nothing and answers the same body. Confirming a chunk twice answers the same record.

### What a batch reports

`GET /worlds/{slug}/data/batches/{batchId}` answers the batch as the import job sees it: its `state`, a `progress` block of `{phase, validatedChunks, appliedRows}`, `entityCounts`, the `snapshotId` and `versionId` it produced, and an `error` when it failed.

| State | Meaning |
| - | - |
| `open` | The batch accepts chunk registrations. |
| `uploading` | At least one chunk is registered; the batch still accepts more. |
| `queued` | Sealed by `…/complete` and handed to the import job. |
| `validating` | Every chunk's rows are being checked against the contract. |
| `applying` | The rows are going onto the world's snapshot. |
| `applied` | Every row landed. A `publish: "later"` batch waits here for `POST …/data/publish`. |
| `refused` | A row broke the contract. Nothing was applied under the default `atomic`. |
| `published` | The rows are in a data-only version. |
| `failed` | The job could not finish; `error` says why. |

`applied`, `refused` and `published` are final: the job never touches the batch again.

A refusal record is `{chunk, index, entity, primaryKey, field, pointer, detail}` — the chunk's ordinal, the 0-based line inside it, and the JSON pointer at the value that broke the contract. The batch view carries the first 200 in `refusals`; `reportUrl` is a presigned link to every record as JSONL.

### Run the tests a world ships

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/worlds/{slug}/tests` | The test runs, newest first — from a push, the CLI, or this API alike. |
| `POST` | `/worlds/{slug}/tests` | Run the suite against the head version, or a pinned one. |
| `GET` | `/worlds/{slug}/tests/{runId}` | One run with its full report. |

Send `{"wait": false}` on the `POST` and the call answers at once with the queued run; read `GET /worlds/{slug}/tests/{runId}` until it settles. The default holds the request open until the suite finishes.

```bash theme={null}
curl "$GATEWAY_HOST/api/public/worlds/acme-crm/tests" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"wait": false}'
```

## World sessions

A session is one live copy of a world, pinned to a version and opened on a task. Opening returns immediately with `state: PROVISIONING`; poll until `ready` is true, then drive it through `/calls`.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/world-sessions` | Open a session: `{ slug` or `versionId, task, surfaces?, reuseWarm?, browserTier? }`. `browserTier` is `standard` (spot, may be reclaimed) or `premium` (on-demand) for a World Host browser; left out, the organization's default. Answers the session id, `instruction`, the task's `tools`, the `surfaces` URLs (`surfaces.browser.tier` names the tier). |
| `GET` | `/world-sessions/{sessionId}` | Is the container warm yet? State, readiness and surfaces. |
| `PATCH` | `/world-sessions/{sessionId}` | `{ "pinned": true }` keeps the session out of the idle reaper, so its URL stays up until it is closed; `false` unpins. |
| `DELETE` | `/world-sessions/{sessionId}` | End the session. An optional body `{ "keepWarm": true }` hands the container back to the warm pool instead of tearing it down. |
| `POST` | `/world-sessions/{sessionId}/calls` | Enqueue a tool call, or `grade`, `seed`, `reset`, `state`, `close`. Returns at once; the container claims the row on its next poll. |
| `GET` | `/world-sessions/{sessionId}/calls` | The session's call log. |
| `GET` | `/world-sessions/{sessionId}/calls/{callId}` | One call's result, once the container has answered. |
| `POST` | `/world-sessions/{sessionId}/task` | Save the session's inline task as a stored task, so the next open can name it by id. |

`task` takes one of three shapes: a bundled task's name, a stored task reference `{ id, version? }`, or the session's own spec `{ instruction, seed?, grader?, metadata?, name? }`. Every response carries a `task` block naming which it was, its content hash, and any stored id and version.

`surfaces` asks for more than `tools`: `api` for the world's service, `ui` for its dashboard, `browser` for a hosted browser driving that dashboard. Asking for one the world does not have answers `400` rather than serving fewer. A schema world whose tree carries `connector.toml` answers `surfaces.api` from the World Host even when `surfaces` did not ask for it, because one session serves both surfaces of the same world.

<Info>
  A World Host session's `api` surface also carries a `token`. Send it as
  `Authorization: Bearer <token>` in a request header on every call to the API
  URL — never in the query string.
</Info>

## Stored world tasks

A stored task keeps an instruction, a seed and a grader as a project resource rather than inside a bundle, so many sessions open on it by id and every grade attributes to the task and its version.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/world-tasks` | The project's stored tasks, newest first. Filter with `world=<slug>` and with `metadata.<key>=<value>` pairs, repeatable. Page with `page` and `limit` (max 100, default 50). |
| `POST` | `/world-tasks` | Store a task at version 1. |
| `GET` | `/world-tasks/{taskId}` | The task, its current (or `?version=N`) spec, and its version history. |
| `PATCH` | `/world-tasks/{taskId}` | A changed spec becomes the next version; name and world pin change in place. |
| `DELETE` | `/world-tasks/{taskId}` | Remove the task and every version. Open sessions keep their copy. |
| `POST` | `/world-tasks/{taskId}/validate` | Check the seed and grader entities against a world version. Reads only; nothing is provisioned. |

## Worlds as versioned bundles

Pushing, pinning, branching and dispatching hosted runs live under `/benchmark-containers`, the older spelling of the same object. See [Worlds hub](/sdk/benchmark-hub) for the Python client and [bench & versions](/cli/bench) for the CLI.

## Project files

The project's file drive, addressed by path. Bytes move over presigned URLs; the app never proxies them. The same routes back the Files tab, `gateway files …` and the MCP tools `list_files`, `get_file`, `request_file_upload`, `confirm_file_upload`, `delete_file`, `move_file`, `create_folder`, `list_file_connectors` and `sync_file_connector`.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/files` | List a folder: `prefix`, `page` (0-based), `limit` (max 200). Returns `folders`, `files` (path, name, sizeBytes, contentType, sha256, author, updatedAt) and `totalFiles`. |
| `POST` | `/files` | Request an upload: `{ path, contentLength, contentType, sha256, overwrite? }` where `sha256` is base64. Returns `{ fileId, uploadUrl, path }`. PUT the bytes to `uploadUrl` with `Content-Type`, `Content-Length` and `x-amz-checksum-sha256`. |
| `POST` | `/files/confirm` | `{ fileId, ok }` — `true` marks the file ready, `false` discards it. |
| `GET` | `/files/download` | `?path=` — metadata and a time-limited GET URL: `{ path, url, urlExpiry, sizeBytes, contentType, sha256 }`. Public key accepted. |
| `DELETE` | `/files` | `?path=` — a file, or a folder and everything under it. Returns `{ deleted }`. |
| `POST` | `/files/move` | `{ fromPath, toPath }` — rename or move a file or folder; metadata only. Returns `{ moved }`. |
| `POST` | `/files/folders` | `{ path }` — create an empty folder. Idempotent. |
| `GET` | `/files/connectors` | The connectors a human shared with agents: id, name, kind, connectionId, targetPrefix, lastSyncAt, lastStatus. |
| `POST` | `/files/connectors/sync` | `{ connectorId }` — run one connector's sync now. Returns `{ files, status, capture, warnings }`; a connector without agent access is `400`. |

```bash theme={null}
curl -X POST "$GATEWAY_HOST/api/public/files" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "content-type: application/json" \
  -d '{"path":"seeds/rows.json","contentLength":24,"contentType":"application/json","sha256":"<base64 sha256>"}'
```

The same drive is reachable per environment at `/environments/{containerId}/files…` with the same shapes. Writes need a secret key.

## Dashboards

A design dashboard's code: TypeScript render files, an entrypoint and an optional transform. The same routes back `gateway dashboards …` ([Dashboards from the CLI](/cli/dashboards)) and the MCP tools `list_dashboards`, `create_dashboard`, `pull_dashboard`, `push_dashboard`, `test_dashboard_transform` and `preview_dashboard`.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/dashboards` | `?limit=` (1–200, default 50). `{ data }`: id, name, kind (`session`, `task`, `environment`), environmentId, taskSetIds, hasCode, updatedAt, url. Public key accepted. |
| `POST` | `/dashboards` | `{ name, description?, taskSetId?, environmentId?, template? }` (`starter` by default, or `empty`). `201` with the dashboard's code, as `GET /dashboards/{id}`. |
| `GET` | `/dashboards/{id}` | The code: `files` (`path`, `content`), `entrypoint`, `transform`, `compiledOnly`, and `stamp`, the value a push sends back. Public key accepted. |
| `PUT` | `/dashboards/{id}/code` | `{ files, entrypoint?, transform?, baseStamp, force?, dryRun? }`. Replaces the render files and transform in one write. `400 dashboard_code_rejected` lists every problem; `409 dashboard_moved` (with the current `stamp`) when the dashboard changed since `baseStamp`. Either refusal writes nothing. |
| `POST` | `/dashboards/{id}/test-transform` | `{ transform?, sessionId? }` — runs the transform on a session; `{ success, sessionId, output, truncated, shape, error }`. Session dashboards only. |
| `POST` | `/dashboards/{id}/render` | `{ sessionId?, taskSetId?, taskId?, includeHtml? }` — renders the saved code; `{ success, error, kind, target, html, url }`. Records nothing. A dashboard with no code yet answers `success: false`. |

Every write and every route that runs code needs a secret key.

## Traces

Traces are the top-level record of one agent run. List them with filters, or fetch one by id to get its scores and full observation tree.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/traces` | List traces. Filter by `userId`, `name`, `tags`, `sessionId`, `environment`, `version`, `release`, `fromTimestamp`, `toTimestamp`. Page with `page` and `limit`. |
| `GET` | `/traces/{traceId}` | Get one trace with its `scores` and `observations` arrays. |
| `POST` | `/traces` | Create or update a trace. |
| `DELETE` | `/traces/{traceId}` | Delete one trace. `DELETE /traces` deletes a list of traces by id. |

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/traces" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "userId=user-42" \
  --data-urlencode "limit=20"
```

The list endpoint also takes a `fields` parameter to control how much of each trace comes back. Request `core`, `io`, `scores`, `observations`, or `metrics` as a comma-separated list to trim the payload.

## Sessions

A session groups the traces of a multi-turn conversation. List sessions in a time range, or fetch one to walk its traces.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/sessions` | List sessions. Filter by `fromTimestamp`, `toTimestamp`, `environment`. Page with `page` and `limit`. |
| `GET` | `/sessions/{sessionId}` | Get one session and its traces. Add `?includeIO=true` to attach every trace's full, untruncated observations. |

With `includeIO=true`, each trace arrives with its observations, inputs and outputs intact, ready to dump as a test fixture.

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/sessions/my-session-id" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "includeIO=true"
```

## Observations

Observations are the steps inside a trace: generations, spans, tool calls, and events. List them with filters or fetch one by id.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/observations` | List observations. Filter by `type`, `name`, `userId`, `level`, `traceId`, `version`, `parentObservationId`, `environment`, `fromStartTime`, `toStartTime`. |
| `GET` | `/observations/{observationId}` | Get one observation with usage, cost, and model details. |
| `GET` | `/v2/observations` | List with cursor pagination (`cursor`, `limit` up to 1000) and selectable `fields`. |

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/observations" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "traceId=my-trace-id" \
  --data-urlencode "type=GENERATION"
```

## Scores

Scores attach an evaluation to a target. Create them one at a time, or list and delete them.

<Info>
  A score targets exactly one thing. Set exactly one of `traceId`, `sessionId`, or `datasetRunId` on the body: never more than one. An `observationId` may accompany a `traceId` to point the score at a specific step within that trace.
</Info>

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/scores` | Create a score. Body: `name`, `value`, optional `dataType`, exactly one of `traceId` / `sessionId` / `datasetRunId`, optional `comment`, `configId`, `metadata`. |
| `GET` | `/scores` | List scores. Filter by `name`, `userId`, `configId`, `queueId`, `traceTags`, `dataType`, `source`, `value` + `operator`, `fromTimestamp`, `toTimestamp`, `scoreIds`. |
| `GET` | `/scores/{scoreId}` | Get one score. |
| `DELETE` | `/scores/{scoreId}` | Delete one score. |
| `GET` | `/v2/scores` | List trace, session, and dataset-run scores together. Create through `POST /scores`, which already accepts all three target types. |
| `GET` `POST` | `/score-configs` | List or create score configurations (the value schema an evaluator writes against). |
| `GET` `PATCH` | `/score-configs/{configId}` | Get or update one score configuration. |

The `dataType` selects the value shape: `NUMERIC` takes a number, `CATEGORICAL` a string, and `BOOLEAN` a `0` or `1`. Omit `dataType` to let the value type decide.

```bash theme={null}
curl "$GATEWAY_HOST/api/public/scores" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "helpfulness",
    "value": 0.9,
    "dataType": "NUMERIC",
    "traceId": "my-trace-id",
    "comment": "Answered the question directly."
  }'
```

<Info>
  The v1 `GET /scores` list returns trace scores, each carrying its trace's `userId`, `tags`, and `environment`. Use `GET /v2/scores` for session and dataset-run scores in the same list.
</Info>

## Datasets, dataset items, and dataset run items

Datasets hold evaluation inputs; runs record what an agent produced against them. The current dataset routes are versioned `v2`; the unversioned routes remain for backward compatibility.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/v2/datasets` | Create a dataset. Body: `name`, optional `description`, `metadata`, `inputSchema`, `expectedOutputSchema`. |
| `GET` | `/v2/datasets` | List datasets. |
| `GET` | `/v2/datasets/{datasetName}` | Get one dataset. |
| `GET` | `/datasets/{name}/runs` | List the runs of a dataset. |
| `GET` | `/datasets/{name}/runs/{runName}` | Get one run with its run items. |
| `DELETE` | `/datasets/{name}/runs/{runName}` | Delete one run. |
| `POST` | `/dataset-items` | Create or update a dataset item. Body: `datasetName`, optional `input`, `expectedOutput`, `metadata`, `id`, `sourceTraceId`. |
| `GET` | `/dataset-items` | List items. Filter by `datasetName`, `sourceTraceId`, `sourceObservationId`. |
| `GET` `DELETE` | `/dataset-items/{datasetItemId}` | Get or delete one item. |
| `POST` | `/dataset-run-items` | Link a trace to a run. Body: `runName`, `datasetItemId`, and one of `traceId` or `observationId`. |
| `GET` | `/dataset-run-items` | List run items. Requires `datasetId` and `runName`. |

```bash theme={null}
curl "$GATEWAY_HOST/api/public/dataset-items" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "datasetName": "support-questions",
    "input": { "question": "How do I reset my password?" },
    "expectedOutput": "Direct them to the reset link."
  }'
```

## Annotation queues

Annotation queues route traces and sessions to human reviewers. List queues, then read or fill their items.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/annotation-queues` | List queues. Page with `page` and `limit`. |
| `POST` | `/annotation-queues` | Create a queue. Body: `name`, `description`, `scoreConfigIds`. |
| `GET` | `/annotation-queues/{queueId}` | Get one queue. |
| `GET` | `/annotation-queues/{queueId}/items` | List items. Filter by `status`. |
| `POST` | `/annotation-queues/{queueId}/items` | Add an item. Body: `objectId`, `objectType`, optional `status`. |
| `GET` `PATCH` `DELETE` | `/annotation-queues/{queueId}/items/{itemId}` | Get, update the status of, or remove one item. |
| `POST` `DELETE` | `/annotation-queues/{queueId}/assignments` | Assign or unassign a reviewer by `userId`. |

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/annotation-queues/my-queue-id/items" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "status=PENDING"
```

## Context Hub

Context Hub stores versioned repositories of agent definitions, prompts, skills, and memory. Each `kind` (`agents`, `prompts`, `skills`, or `memory`) shares one route shape.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/context/{kind}` | List the repos of that kind. |
| `GET` | `/context/{kind}/{identifier}` | Pull the latest version. Add `?version=` for a specific one, or `?exists=true` to check presence. |
| `POST` | `/context/{kind}/{identifier}` | Push a new version of the repo. |
| `DELETE` | `/context/{kind}/{identifier}` | Delete the repo. |

```bash theme={null}
curl "$GATEWAY_HOST/api/public/context/agents/my-agent" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY"
```

Context Hub items are versioned agent, prompt, skill and memory repositories; traces stamp the items an execution used (see [Tracing reference](/sdk-reference/tracing)).

## User profiles

User profiles store the identity and attributes behind a trace's `userId`, tying usage and cost back to a real account.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/user-profiles/{userId}` | Get one profile by the id that appears on traces. |
| `POST` | `/user-profiles` | Create or update a profile. Body: `userId`, optional `displayName`, `email`, `attributes`, `notes`, `mergeAttributes`. |

By default a `POST` merges new `attributes` into the existing set, so incremental updates are one call. Set `mergeAttributes` to `false` to replace the attribute set instead.

```bash theme={null}
curl "$GATEWAY_HOST/api/public/user-profiles" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-42",
    "displayName": "Ada Lovelace",
    "attributes": { "plan": "enterprise" }
  }'
```

## User events

User events record the value a user (or the agent acting for them) produced: the per-user ROI stream. `value` is the event's worth in your own terms (minutes saved, revenue); Surface Area sums it in the ROI and hours-saved rollups, and evals and task-set replays can assert against the stream.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/user-events` | Record one event. Body: `userId`, `event`, optional `value`, `metadata`, `traceId`, `sessionId`. |
| `GET` | `/user-events` | List events. Query: `userId`, `event`, `fromTimestamp`, `toTimestamp`, `page`, `limit`. |

```bash theme={null}
curl "$GATEWAY_HOST/api/public/user-events" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user-42",
    "event": "ticket_resolved",
    "value": 12,
    "sessionId": "sess-123"
  }'
```

## Agent builds

Agent builds are immutable, content-addressed agent identities: the SDK
registers one automatically at `tracing.init({ agent, version })`, and a
release is a label move, never a mutation. Rolling back across a breaking
tool change is refused unless forced.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/agent-builds` | Register a build (idempotent on `buildHash`). Body: `agentId`, `buildHash`, optional `semanticVer`, `manifest`, `attribution`. |
| `GET` | `/agent-builds` | List builds. Query: `agentId`, `page`, `limit`. |
| `GET` | `/agent-builds/resolve` | Resolve a label to its build. Query: `agentId`, `label`. |
| `GET` | `/agent-builds/diff` | Per-tool/prompt/model delta between two builds. Query: `agentId`, `base`, `candidate`. |
| `POST` | `/agent-builds/{buildId}/labels` | Move a release label. Body: `label`, `force`, `reason`, `actor`, `source`. |
| `GET` | `/agent-builds/{buildId}/gate` | The release gate's verdict on a build: `pass`, `blocked`, or `untested`. Read-only; moves nothing. |
| `GET` | `/agent-builds/gate` | The same gate, looked up by `agentId` + `version` or by `commitSha` (full sha or a 7+ character prefix). Name the build exactly one way. |

### The release gate

The gate is the rule the Releases page applies before a promotion: the pass
rate over graded rollouts must reach the project's threshold, and the graded
rollout count must reach its floor (default 0.70 and 30; edit both under
Releases). The gate endpoint applies that rule to every completed run
attributed to the build, and to other builds registered from the same commit,
so a pipeline and the Releases page agree.

| Verdict | Meaning | `gateway release gate` exit code |
| - | - | - |
| `pass` | The gate is open. | 0 |
| `blocked` | The completed runs fail the rule; `reasons` names each condition and by how much. A run that ended without completing is blocked, not untested. | 1 |
| `untested` | Nothing to judge yet: no run has recorded this build, or a run is still going. | 2 |

Missing credentials exit 1, never 2, so a pipeline that waits on `untested` fails when its secret is not set.

```bash theme={null}
curl "$GATEWAY_HOST/api/public/agent-builds/gate?commitSha=$GITHUB_SHA" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY"
```

```json theme={null}
{
  "build": {
    "id": "cm1abc...",
    "agentId": "checkout-bot",
    "version": "2.4.0",
    "buildHash": "sha256:9f2c...",
    "commitSha": "0123456789abcdef0123456789abcdef01234567",
    "branch": "main"
  },
  "gate": {
    "verdict": "blocked",
    "passRate": 0.75,
    "gradedRollouts": 12,
    "threshold": 0.7,
    "minGradedRollouts": 30,
    "reasons": ["only 12 graded, floor is 30"]
  },
  "evaluatedRuns": [
    {
      "runId": "cm1run...",
      "world": "drive-sim@1.2.0",
      "state": "COMPLETED",
      "model": "gpt-4o-mini",
      "passRate": 0.75,
      "graded": 12
    }
  ],
  "comparedTo": { "stableBuildId": "cm1prev...", "stableVersion": "2.3.0" }
}
```

`passRate` is `null` (never `0`) when nothing was graded. `comparedTo` is the
newest production release before this build, or `null` when there is none.
The CLI wrapper is `gateway release gate`, whose `--wait` polls until every run
attributed to the build is terminal and whose exit code CI branches on. See
[The gateway CLI](/cli).

## Media

Media endpoints handle images, audio, and other binary assets attached to traces and observations. The upload flow requests a URL, uploads to it, then confirms.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/media` | Request a presigned upload URL for a media asset. |
| `GET` | `/media/{mediaId}` | Get a media record and its download URL. |
| `PATCH` | `/media/{mediaId}` | Update a media record after the upload finishes. |

```bash theme={null}
curl "$GATEWAY_HOST/api/public/media/my-media-id" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY"
```

## Metrics

Metrics endpoints aggregate traces, observations, and scores into numbers. One endpoint runs a flexible query; the other returns daily usage and cost buckets.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/metrics` | Run an aggregation. Pass a URL-encoded JSON `query` object. |
| `GET` | `/metrics/daily` | Daily counts, usage, and cost. Filter by `traceName`, `userId`, `tags`, `environment`, `fromTimestamp`, `toTimestamp`. |

The `query` object on `GET /metrics` takes a `view`, an array of `metrics`, optional `dimensions`, `filters`, and `timeDimension`, plus `fromTimestamp` and `toTimestamp`. The response is a `{ "data": [...] }` array without a pagination `meta` block. The `metric` and `dimension` element shapes are documented in the [Evaluation guide](/evaluation) and the Fern API specs.

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/metrics/daily" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "fromTimestamp=2024-01-01T00:00:00Z" \
  --data-urlencode "toTimestamp=2024-02-01T00:00:00Z"
```

## Models

Model definitions map a model name to its pricing so Surface Area can compute cost. The list mixes your project's custom models with the built-in global definitions.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/models` | List model definitions: project models plus built-in globals. |
| `POST` | `/models` | Create a custom model definition with a `matchPattern` and prices. |
| `GET` `DELETE` | `/models/{modelId}` | Get or delete one custom model definition. |

```bash theme={null}
curl -G "$GATEWAY_HOST/api/public/models" \
  -u "$GATEWAY_PUBLIC_KEY:$GATEWAY_SECRET_KEY" \
  --data-urlencode "limit=50"
```

## More route groups

Surface Area exposes further public route groups: benchmarks (`/benchmark-runs`, `/benchmark-tasks`, `/benchmark-containers`), environments and the environment agent, the coding-agent runtime (`/agents`, `/v1/agents`, `/agent-runtime`), automations, experiment runs (`/runs`, `/experiments`), prompt management (`/prompts`, `/v2/prompts`), personas, secrets, comments, LLM connections, MCP gateways, integrations (Merge, blob storage, tool sessions, access requests, approvals, webhooks), Slack, and SCIM user and group provisioning.

A few administrative route groups (organizations, at `/organizations/*`, and cross-project management, at `/projects`) are scoped to an **organization** key rather than a project key.


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