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

# API introduction

> Use the SignalMD API to read issues, calls and versions from your own tools, and to wire SignalMD into CI.

The SignalMD API gives your own tools the data the web app shows: agents, calls and their traces, signals, issues, versions, comparisons, tests and runs. With a write key it can also create and edit tests and start runs. The [MCP server](/mcp/overview) calls this same API.

The reference in this tab is generated from the API, so it lists the operations that exist now. This page covers what every operation has in common.

## Base URL

```text theme={null}
https://api.signalmd.dev/v1
```

Every path in the reference starts with `/v1`. The OpenAPI document is at `GET /v1/openapi.json` and needs no key.

## Make your first request

[Make a key in the app](/api/authentication#make-a-key), then ask the API who you are. The values below are examples.

```bash Terminal theme={null}
export SIGNAL_API_KEY="sig_your_key"

curl -i https://api.signalmd.dev/v1/me \
  -H "Authorization: Bearer $SIGNAL_API_KEY"
```

```json Response body, read key theme={null}
{
  "object": "principal",
  "kind": "key",
  "organizationId": "org_2example",
  "userId": null,
  "key": {
    "id": "3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
    "scope": "read",
    "agentId": null,
    "monthlyBudgetUsd": null
  },
  "rateLimits": {
    "all": { "perMinute": 600, "burst": 120 },
    "write": { "perMinute": 60, "burst": 20 }
  }
}
```

A `401` means the key is missing, wrong or revoked. See [Authentication](/api/authentication).

## Resources

Every path except `/v1/me` and `/v1/agents` starts with `/v1/agents/{agentId}`, so find the agent id first.

| Resource | What it holds | Operations |
| - | - | - |
| Account | The key, its scope and budget, your rate limits | [Who am I](/api-reference/account/who-am-i) |
| Agents | The voice agents connected to the workspace | [List agents](/api-reference/agents/list-agents), get an agent |
| Calls | Imported production calls, their traces and signals | [List calls](/api-reference/calls/list-calls), get a call, its trace, its signals |
| Signals | Each signal code seen across calls, with counts | [List signals](/api-reference/signals/list-signals) |
| Issues | Diagnosed problems, with the calls behind them | [List issues](/api-reference/issues/list-issues), get an issue, its evidence calls |
| Versions | Agent versions, outcome rates, the difference between two | [List versions](/api-reference/versions/list-versions), get a version, [compare two](/api-reference/versions/compare-two-versions) |
| Comparisons | Stored measurements of one version against another | [List comparisons](/api-reference/comparisons/list-version-comparisons), get one |
| Tests | Tests, test sets, personas and coverage | [List tests](/api-reference/tests/list-tests), get a test, revisions, spec schema, [validate a spec](/api-reference/tests/validate-a-test-spec), test sets, personas, coverage. **Write:** [create a test](/api-reference/tests/create-a-test), replace a spec |
| Runs | Test runs, their simulated calls, and results | [List runs](/api-reference/runs/list-runs), get a run, its result, its attempts, get an attempt. **Write:** [start a run](/api-reference/runs/start-a-run), rescore a run, [calibrate tests](/api-reference/runs/calibrate-tests) |

Validating a spec is a `POST` that stores and spends nothing, so a read key can call it. Scope follows what an operation does, not its HTTP method.

**Calibrate tests** needs a signed-in session. Every API key is refused with `403` and the code `people_only`, whatever its scope. It plays each test three times and spends like a run.

For the order to call things in, see [Fix an issue](/guides/fix-an-issue) and [Test a fix](/guides/test-a-fix).

## Conventions

* JSON in and out, with camelCase fields. Ids are UUIDs. Timestamps are ISO 8601 in UTC, such as `2026-10-02T14:03:07.120Z`.
* Every response has a `Request-Id` header (`req_` and 24 hex characters), repeated in error bodies. Quote it when you report a problem.
* A path under `/v1/agents/{agentId}` returns `404` for an agent that does not exist, belongs to another workspace, or is not the one your key is tied to.

## Errors

Every response that is not a success has the same body.

```json Response body, 400 theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_cursor",
    "message": "This cursor is malformed. Pass `nextCursor` from a previous page unchanged.",
    "param": "cursor",
    "requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
  }
}
```

Branch on `type`, which follows from the status: `invalid_request_error` (400, 415, 422), `authentication_error` (401), `budget_error` (402), `permission_error` (403), `not_found_error` (404), `conflict_error` (409), `rate_limit_error` (429) and `api_error` (500). `code` names the reason and is stable within `/v1`. `message` is for people, so do not parse it. `param` names the field at fault, or is `null`. New codes can appear: treat an unfamiliar `code` as the general case of its `type`.

| Status | `code` | When |
| - | - | - |
| 400 | `invalid_parameter`, `invalid_cursor`, `invalid_body`, `invalid_request` | A field failed validation, the cursor is not from a previous page, the body is not JSON, or the request was refused. `param` names the field |
| 400 | `invalid_spec`, `invalid_run`, `tests_archived` | A test spec has problems ([validate a spec](/api-reference/tests/validate-a-test-spec) lists them all), the run was refused (for example, a persona the agent does not have), or it names an archived test. Nothing runs |
| 401 | `missing_authorization`, `invalid_api_key`, `invalid_token` | No bearer header, an unknown or revoked key, or an expired session token |
| 402 | `quota_exceeded` | The plan's credits for the period do not cover the run. `error.quota` gives `metric`, `activity`, `requested`, `limit`, `used`, `plan` and `upgradePath`. Nothing was spent |
| 402 | `budget_exceeded` | The key's monthly budget or the agent's test budget does not cover the request |
| 402 | `testing_not_configured` | Testing is off for the agent, or it has no Retell test key |
| 403 | `key_read_only`, `people_only`, `demo_read_only` | A read key called a write operation, an API key called a person-only one, or a write to the shared demo agent |
| 404 | `agent_not_found`, `route_not_found`, and a `_not_found` code for each other resource, such as `call_not_found` | The agent, resource or path does not exist for this key |
| 409 | `rescore_not_allowed` | The run cannot be rescored now. A key may rescore only what current scoring would score differently, one run at a time |
| 422 | `not_runnable`, `not_calibratable`, `unprocessable` | The tests cannot be played against that version, nothing could be calibrated, or the request was well formed and refused |
| 429 | `rate_limited`, `too_many_failed_authentications` | A bucket is empty, or this address has failed authentication too often. See `Retry-After` |
| 500, 503 | `internal_error`, `auth_not_configured` | A fault on SignalMD's side, or sign-in is not set up on the server |

A 402 `quota_exceeded` is a plan limit, so do not retry it as it is. [Limits and quotas](/limits#plan-limit-errors) shows the full response.

## Pagination

Operations that return many items take `limit` (1 to 100, default 25) and `cursor`, and return a list.

```json Response body theme={null}
{
  "object": "list",
  "data": [],
  "hasMore": true,
  "nextCursor": "eyJ2Ijoi..."
}
```

Pass `nextCursor` back as `cursor` for the next page, and stop when `hasMore` is `false`. Treat the cursor as opaque and send it back unchanged.

## Rate limits

Each API key, or signed-in person, can send 600 requests a minute with a burst of 120. A write also draws from a tighter bucket of 60 a minute with a burst of 20. Every response carries `RateLimit-Limit` (the bucket size), `RateLimit-Remaining` and `RateLimit-Reset` (seconds until full) for the tighter bucket the request drew from. A refused request is a `429` with `rate_limited` and a `Retry-After` header in seconds. Wait that long, then retry.

The MCP server draws from the same buckets. The workspace as a whole, the AI-backed routes and failed authentications have limits too. [Limits and quotas](/limits#api-rate-limits) lists them all.

## Runs take time

[Start a run](/api-reference/runs/start-a-run), [rescore a run](/api-reference/runs/rescore-a-run) and [calibrate tests](/api-reference/runs/calibrate-tests) answer at once with `202`. The simulated calls play afterwards. Poll [Get a run](/api-reference/runs/get-a-run) until `status` is `complete`, `refused` or `failed`, then read [the result](/api-reference/runs/get-a-runs-result-document).

A run is checked whole against the plan before it starts. If the plan's credits do not cover it, it is refused with `quota_exceeded` and nothing plays. A run SignalMD started itself can show `awaiting_approval` when it is over the agent's monthly test budget. Started through the API, the same run is refused with a `402`. No key can approve a run.

To be told instead of polling, add a webhook endpoint under **API keys** in the app. When a run ends, SignalMD posts its result document to every endpoint, and retries a failed delivery 5 more times over about 7 hours.

## Retries and idempotency

The API takes no idempotency keys. Retrying a read is safe. Retry a `429` after `Retry-After`, and a `500` or a dropped connection with a growing delay. A write that timed out may have taken effect, so read the resource before you send it again.

## Versioning

The version is in the path. Within `/v1`, changes only add: operations, optional parameters, response fields, error codes, and values in a field with a fixed set of values. Ignore fields you do not recognise, and treat an unknown value as "other". Removing or renaming a field, a new required parameter, stricter validation, and any change to the error body or pagination need `/v2`, which would run beside `/v1`.

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api/authentication">
    Make a key, choose read or write, and keep it safe.
  </Card>

  <Card title="Limits and quotas" icon="gauge" href="/limits">
    Plan credits, rate limits and the plan limit error.
  </Card>
</CardGroup>


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