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

# API

> Call Tracks over HTTP with an API key of your organization

The API starts runs and follows them, uploads files, connects MCP servers, creates and launches experiments, and lists your library. The [OpenAPI document](https://tracks.alpic.ai/api/openapi.json) describes every endpoint, to generate a client in any language.

* **Base URL**: `https://tracks.alpic.ai/api`
* **Format**: JSON request and response bodies, with `Content-Type: application/json`.

## Authentication

Create a key in **API keys** in the web app. It starts with `trk_`, is shown once, and acts for your whole organization. Send it on every request:

```http theme={null}
Authorization: Bearer trk_...
```

A missing, malformed or revoked key gets a `401` with `ApiKeyRefused`. `GET /health` and `GET /egress-ips` need no key.

## Start a run

```bash theme={null}
curl https://tracks.alpic.ai/api/runs \
  -H "Authorization: Bearer $TRACKS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: readme-fix-1" \
  -d '{
    "agent": { "harness": "claude-code", "model": "claude-sonnet-5-5" },
    "prompt": "Read the README and list what is missing to run the project locally",
    "environment": { "repository": "https://github.com/alpic-ai/apps-sdk-template" },
    "budget_usd": 5
  }'
```

It answers `201` with the run, its status `preparing`. Then follow it with `GET /runs/{id}/events/stream`, or poll `GET /runs/{id}` until its status is `ended`.

## Errors

An error is a JSON object with a `_tag` naming it and a `message` saying what happened, in plain words:

```json theme={null}
{ "_tag": "RunNotFound", "message": "..." }
```

| Status | `_tag` | When | Retry? |
| - | - | - | - |
| 400 | `InvalidRequest` | A value is missing, has the wrong type or is out of range. Also has `location` (`path`, `query`, `header` or `body`) and `issues`, each with the `path` to the wrong value and a `message`. | No: fix the request. |
| 401 | `ApiKeyRefused` | The key is missing, malformed or revoked, or its organization is closed. | No. |
| 402 | `InsufficientBalance` | Your organization's balance is \$0 or below. Also has `top_up_url`, where to add funds. | After adding funds. |
| 403 | `OrganizationBlocked` | Your organization can't start runs after a dispute on one of its payments. | No: contact us. |
| 404 | `RunNotFound`, `ExperimentNotFound`, `ExecutionNotFound`, `ConnectionNotFound`, `ScreenshotNotFound` | No such item in your organization, or the event has no screenshot. | No. |
| 404 | `RouteNotFound` | No endpoint matches this method and path. | No. |
| 409 | `RunNotIdle` | A follow-up prompt was sent to a run that is one-shot, has ended, or is still working on a prompt. | Once the run is `idle`. |
| 409 | `ConfirmationRefused` | The connection isn't waiting for this confirmation. | No. |
| 422 | `ModelNotOffered` | The agent can't use this model. The message lists the models it can use. | No: pick one of them. |
| 422 | `ConnectionUnusable` | A `connection_id` in `environment.mcp_servers` doesn't exist or isn't `active`. | No. |
| 422 | `FileUnavailable` | An input file is missing, expired, or not your organization's. | No. |
| 422 | `ExperimentIncomplete` | The experiment can't be launched as it is. Also has `missing`, each thing to fix in a sentence. | After fixing it. |
| 422 | `ConnectionRefused` | The connection can't start: the return URL isn't allowed, or the MCP server can't be authorized in a way Tracks supports. | No. |
| 502 | `ServerUnreachable` | The MCP server or its authorization server couldn't be reached. | Yes, later. |
| 503 | `ModelProviderUnavailable` | The model provider couldn't be reached to check the model. | Yes, later. |

Any other `5xx` is an unexpected error on our side: retry later. Retry a `POST /runs` with the same `Idempotency-Key`, so it can't start a second run.

## Retry safely with `Idempotency-Key`

`POST /runs` takes an optional `Idempotency-Key` header: any value you choose, up to 255 characters. A request with a key already used in your organization returns the run the first request created, without starting another, whatever its body. Send one whenever you may retry, after a timeout or a `5xx`.

No other endpoint takes one. Ending or canceling is safe to repeat: on a run that has ended, it does nothing.

## Pagination

Lists return `{ "data": [...], "has_more": true }`. While `has_more` is `true`, ask for the next page by passing the last item received as `after`:

| Endpoint | Order | Pass as `after` |
| - | - | - |
| `GET /runs` | Most recently started first | The last run's `id` |
| `GET /runs/{id}/prompts` | In order | The last prompt's `index` |
| `GET /runs/{id}/frames` | In order | The last frame's `seq` |
| `GET /runs/{id}/events` | In order | The last event's `seq` |

These take `limit`, from 1 to 1000, 100 by default. `GET /runs` also filters by `label=key:value`, repeated to require several labels.

The other lists (experiments, an experiment's executions, an execution's slots, connections) return every item at once, with `has_more` always `false`.

## Follow a run live

Two endpoints send a run's recording as [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) as it happens:

| Endpoint | Sends |
| - | - |
| `GET /runs/{id}/events/stream` | An `event` message per event, its data the event as JSON |
| `GET /runs/{id}/frames/stream` | A `frame` message per frame, its data the frame as JSON |

* Each message's `id` is the event's or frame's `seq`.
* A `heartbeat` message, with empty data, comes every 30 seconds to keep the connection open.
* The connection closes once the run has ended and everything was sent.
* It may also close earlier. To pick up where it stopped, call again with the last `seq` received as `after`, or let your SSE client reconnect: the `Last-Event-ID` header it sends is used when `after` is absent.

To know whether a closed connection means the run ended, check `GET /runs/{id}`.

## Limits

Tracks sets no rate limit on requests and no cap on how many runs work at once. Your balance and each run's budget bound what you spend: runs start only while your balance is above \$0.

| What | Limit |
| - | - |
| Input files | 20 per run, each up to 20 MiB |
| File upload | PUT the bytes within 15 minutes; files not attached to a run expire after 24 hours |
| Labels | 19 of yours per run, values up to 256 characters; `started_by` is set to the API key |
| Variables | 100 per run, values up to 32,768 characters |
| `setup` and `teardown` scripts | 32,768 characters each |
| MCP servers | 20 per run, each with up to 20 headers of up to 8,192 characters |
| Skills | 20 per run, each up to 100 files, 2 MB together |
| Runs per execution | 500: every scenario, variant, agent and repetition |
| `Idempotency-Key` | 255 characters |
| `limit` on lists | 1 to 1000; 100 by default |

## Other endpoints

* `GET /health` answers `204` when the API is up.
* `GET /egress-ips` lists the addresses Tracks calls MCP servers from, to allow on a server that filters callers.


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