# REST API reference

> Every operation of the Belvador API: path, required scope, which ones need a person's approval, retries and version checks; generated from the list the server runs.

**Status: live (beta).** The REST API answers at `https://api.belvador.dev`. This page is generated from the same list of operations the gateway runs, so it changes only when the API does. Tools that support OAuth can connect by sign-in instead of a token. Coding agents can use the same operations through the [MCP server](https://belvador.dev/en/docs/mcp).

## Basics

- **Base URL:** `https://api.belvador.dev/v1`. JSON uses `snake_case`, times are RFC 3339 in UTC, ids are strings.
- **Authentication:** send a personal token (`blv_pat_…`, from **Account → API tokens**) or a service token (`blv_svc_…`, from **Workspace settings → Service accounts**) in the `Authorization` header as `Bearer blv_…` ([credentials](https://belvador.dev/en/docs/security/credentials)). Tokens last at most 365 days. A token works only inside its workspace and the projects and environments it was given, and never above the current role of the person or service account behind it.
- **Sign in with OAuth:** tools that support OAuth 2.1, such as MCP clients, find Belvador from `https://api.belvador.dev/.well-known/oauth-protected-resource` and `https://belvador.dev/.well-known/oauth-authorization-server`. They register themselves (RFC 7591, or a Client ID Metadata Document), send you to belvador.dev to pick a workspace and permissions, and must use PKCE with `S256`. Access tokens (`blv_oat_…`) last 60 minutes; refresh tokens (`blv_ort_…`) change on every use and stop after 30 days without use. A code or refresh token used twice ends that connection — two refreshes sent at the same moment count as reuse, so refresh one at a time. See and disconnect apps under **Account → Connected apps**.
- **Errors** follow RFC 9457 (`application/problem+json`): `code` is the stable error code, `request_id` identifies the call, and `errors[].pointer` names each invalid field, for example `/name`.
- **Creating something** needs an `Idempotency-Key` header. Sending the same key with the same body within 24 hours returns the first answer again, marked `Idempotent-Replayed: true`, and creates nothing new. A one-time secret in that answer (a ping token, a key) is never repeated: the replay shows `null` and names the field in `Belvador-Secret-Withheld`. The same key with a different body is refused with 422 `IDEMPOTENCY_KEY_REUSED`.
- **Changing or deleting** a monitor, uptime check, SLO, project setting or alert rule needs `If-Match` with the `ETag` you last read. Without it the answer is 428; if someone changed it since, 412 — read it again and retry.
- **Lists** return `{ "data": [...], "next_cursor": "...", "complete": true }`. Pass `cursor` with the same filters for the next page; a cursor lasts 24 hours and only works for the list that made it.
- **Sensitive operations** — keys, members, alert destinations, node changes — answer 403 `APPROVAL_REQUIRED` and wait for a person; see [Approvals](#approvals).
- **Data stored on your node** (issues, events, traces, logs, monitor runs) is marked *node data* below. The gateway reads it from your node with a ticket that lasts a few minutes and passes the answer through; Belvador's servers neither store nor log it. If your node is offline the answer is 503 `NODE_OFFLINE` — nothing is served from a copy. Trace and log searches need `start` and `end`, at most 168 hours apart.
- **Rate limits:** 300 calls per minute per token and 1200 per workspace, and 4 node-data calls in flight per token. Every answer carries the IETF `RateLimit-Policy` and `RateLimit` headers; over the limit the answer is 429 `RATE_LIMITED` with `Retry-After`.
- **Turning it off:** revoking a token stops it within about 30 seconds. In an emergency Belvador can pause the API for one workspace or for everyone; calls then answer 503 `PUBLIC_API_PAUSED` and the app keeps working.

## Approvals

Sensitive operations never run straight from a token: DSN, upload and ingest keys, ping tokens, members and invites, tokens and service accounts, alert destinations and node changes. They are marked *approval* in the list below.

1. The call answers 403 `APPROVAL_REQUIRED` with `approval_id`, `approval_url` (a page on belvador.dev), `expires_at` and `state`, and a `Link: <…>; rel="approval"` header. Nothing has changed yet.
2. A person opens the link, sees exactly what will change, and approves with a code from their authenticator app no older than 10 minutes — or rejects. Most operations need an admin; deleting, rebuilding or recovering the node needs the owner; a new personal token can only be approved by the token's own holder. Requests from service accounts and OAuth apps are also emailed to the workspace's owners and admins.
3. Send the same request again with the same `Idempotency-Key`, or poll `GET /v1/approvals/{approval_id}`, to read the outcome: `executed` with the ids it produced (200), `rejected`, `expired` after 15 minutes (410), or `failed` with the reason in `code`.

What always holds:

- It runs exactly the request the person saw, once, as that person. If the stored request no longer matches, or the token was revoked or lost its rights meanwhile, it fails and nothing runs.
- Secrets it creates — a new key, token or invite link — are shown once to the approving person, never to the caller.
- What a person types themselves — the project or node name that confirms a deletion, a bot token, webhook URL or mail password — is never accepted from the caller.
- At most 20 requests wait per workspace; more answer 429 `RATE_LIMITED`.
- `failed` with `APPROVAL_OUTCOME_UNKNOWN` means it was approved but its result was never recorded. Check the resource before asking again.

## Operations

### `alert_deliveries`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/alert-deliveries/{delivery_id}/replay` | `alerts:write` | admin | — |

### `alert_rules`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/alert-rules` | `alerts:read` | viewer | paged |
| `POST /v1/alert-rules` | `alerts:write` | admin | — |
| `PUT /v1/alert-rules/{rule_id}/enabled` | `alerts:write` | admin | — |
| `DELETE /v1/alert-rules/{rule_id}` | `alerts:write` | admin | `If-Match` |

### `alerts`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/alerts` | `alerts:read` | viewer | paged |

### `approvals`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/approvals/{approval_id}` | `org:read` | viewer | — |

### `audit`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/audit-log` | `audit:read` | admin | paged |

### `coverage`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/coverage` | `coverage:read` | viewer | — |
| `PUT /v1/projects/{project_id}/coverage-expectations` | `coverage:write` | member | — |

### `deployments`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/projects/{project}/deployments` | `releases:write` | member | node data |

### `destinations`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/destinations` | `alerts:read` | viewer | paged |
| `POST /v1/destinations` | `destinations:manage` | admin | approval · `Idempotency-Key` |
| `PATCH /v1/destinations/{destination_id}` | `destinations:manage` | admin | approval |
| `POST /v1/destinations/{destination_id}/connection` | `destinations:manage` | admin | approval |
| `DELETE /v1/destinations/{destination_id}` | `destinations:manage` | admin | approval |
| `PUT /v1/destinations/{destination_id}/recipients` | `destinations:manage` | admin | approval |
| `POST /v1/destination-recipients/{recipient_id}/resend` | `destinations:manage` | admin | — |
| `POST /v1/destinations/{destination_id}/test` | `notifications:write` | admin | — |

### `events`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/issues/{issue_id}/events` | `events:read` | viewer | node data · paged |
| `GET /v1/issues/{issue_id}/events/{event_id}` | `events:read` | viewer | node data |

### `incident_rules`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/incident-rules` | `alerts:read` | viewer | — |
| `POST /v1/incident-rules` | `alerts:write` | admin | — |
| `DELETE /v1/incident-rules/{rule_id}` | `alerts:write` | admin | — |

### `incidents`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/incidents` | `incidents:read` | viewer | paged |
| `GET /v1/incidents/{incident_id}` | `incidents:read` | viewer | — |
| `POST /v1/incidents/{incident_id}/ack` | `incidents:write` | member | — |
| `POST /v1/incidents/{incident_id}/resolve` | `incidents:write` | member | — |
| `POST /v1/incidents/{incident_id}/notes` | `incidents:write` | member | — |
| `PATCH /v1/incidents/{incident_id}` | `incidents:write` | member | — |

### `ingest_keys`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/projects/{project_id}/ingest-keys` | `keys:manage` | member | approval · `Idempotency-Key` |
| `DELETE /v1/ingest-keys/{key_id}` | `keys:manage` | member | approval |

### `invites`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/invites` | `members:manage` | admin | approval · `Idempotency-Key` |
| `DELETE /v1/invites/{invite_id}` | `members:manage` | admin | — |

### `issues`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/issues` | `issues:read` | viewer | node data · paged |
| `GET /v1/issues/{issue_id}` | `issues:read` | viewer | node data |
| `GET /v1/issues/{issue_id}/stats` | `issues:read` | viewer | node data |
| `GET /v1/issues/{issue_id}/tags` | `issues:read` | viewer | node data |
| `GET /v1/issues/{issue_id}/activity` | `issues:read` | viewer | node data |
| `GET /v1/issues/{issue_id}/fix-status` | `releases:read` | viewer | node data |
| `PATCH /v1/issues/{issue_id}` | `issues:write` | member | node data |
| `POST /v1/issues/bulk` | `issues:write` | member | node data |

### `logs`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/logs` | `logs:read` | viewer | node data · `start`/`end` · paged |
| `GET /v1/logs/histogram` | `logs:read` | viewer | node data · `start`/`end` |
| `GET /v1/logs/facets` | `logs:read` | viewer | node data · `start`/`end` |
| `GET /v1/logs/{log_id}/around` | `logs:read` | viewer | node data |

### `maintenance`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/maintenance-windows` | `monitors:read` | viewer | paged |
| `POST /v1/maintenance-windows` | `monitors:write` | member | `Idempotency-Key` |
| `PATCH /v1/maintenance-windows/{window_id}` | `monitors:write` | member | — |
| `DELETE /v1/maintenance-windows/{window_id}` | `monitors:write` | member | — |

### `members`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/members` | `members:read` | viewer | paged |
| `PATCH /v1/members/{user_id}` | `members:manage` | admin | approval |
| `DELETE /v1/members/{user_id}` | `members:manage` | admin | approval |

### `metrics`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/metrics/names` | `metrics:read` | viewer | node data |
| `GET /v1/metrics/series` | `metrics:read` | viewer | node data · paged |
| `GET /v1/metrics/query` | `metrics:read` | viewer | node data · `start`/`end` |

### `monitors`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/monitors` | `monitors:read` | viewer | paged |
| `GET /v1/monitors/{monitor_id}` | `monitors:read` | viewer | — |
| `POST /v1/monitors` | `monitors:write` | member | `Idempotency-Key` |
| `PATCH /v1/monitors/{monitor_id}` | `monitors:write` | member | `If-Match` |
| `DELETE /v1/monitors/{monitor_id}` | `monitors:write` | member | `If-Match` |
| `PUT /v1/monitors/{monitor_id}/pause` | `monitors:write` | member | — |
| `PUT /v1/monitors/{monitor_id}/mute` | `monitors:write` | member | — |
| `DELETE /v1/monitor-discoveries/{discovery_id}` | `monitors:write` | member | — |
| `GET /v1/monitors/{monitor_id}/signal` | `monitors:read` | viewer | node data |
| `GET /v1/monitors/{monitor_id}/occurrences` | `monitors:read` | viewer | node data · paged |
| `POST /v1/monitors/backtest` | `monitors:read` | viewer | node data |

### `node`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/node` | `org:read` | viewer | — |
| `POST /v1/node` | `node:operate` | admin | approval · `Idempotency-Key` |
| `POST /v1/node/jobs/{job_id}/retry` | `node:operate` | admin | — |
| `POST /v1/node/{node_id}/resize` | `node:operate` | admin | approval |
| `POST /v1/node/{node_id}/upgrade` | `node:operate` | admin | — |
| `DELETE /v1/node/{node_id}` | `node:operate` | admin | approval |
| `POST /v1/node/{node_id}/restore` | `node:operate` | admin | — |
| `POST /v1/node/backups` | `node:operate` | admin | — |
| `PUT /v1/node/backup-custody` | `node:operate` | admin | approval |
| `POST /v1/node/{node_id}/rebuild` | `node:operate` | admin | approval · `Idempotency-Key` |
| `POST /v1/node/jobs/{job_id}/recovery-kit` | `node:operate` | admin | approval |

### `notification_policies`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/notification-policies` | `alerts:read` | viewer | — |
| `POST /v1/notification-policies` | `alerts:write` | admin | — |
| `DELETE /v1/notification-policies/{policy_id}` | `alerts:write` | admin | — |

### `occurrences`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/occurrences` | `monitors:write` | member | node data |
| `DELETE /v1/occurrences/{monitor_slug}/{occurrence_id}` | `monitors:write` | member | node data |

### `ping_tokens`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/monitors/{monitor_id}/ping-tokens/rotate` | `keys:manage` | member | approval |
| `POST /v1/monitors/{monitor_id}/ping-tokens/revoke-previous` | `keys:manage` | member | approval |
| `POST /v1/monitors/{monitor_id}/ping-tokens/reset` | `keys:manage` | member | approval |

### `privacy`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/projects/{project_id}/privacy` | `projects:read` | viewer | — |
| `PUT /v1/projects/{project_id}/privacy` | `privacy:write` | admin | approval |

### `project_keys`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/projects/{project_id}/keys` | `keys:manage` | member | approval · `Idempotency-Key` |
| `POST /v1/keys/{key_id}/rotate` | `keys:manage` | member | approval |
| `DELETE /v1/keys/{key_id}` | `keys:manage` | member | approval |

### `projects`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/projects` | `projects:read` | viewer | paged |
| `GET /v1/projects/{project_id}` | `projects:read` | viewer | — |
| `POST /v1/projects` | `projects:write` | member | `Idempotency-Key` |
| `PATCH /v1/projects/{project_id}` | `projects:write` | member | `If-Match` |
| `PATCH /v1/projects/{project_id}/intake` | `projects:write` | member | `If-Match` |
| `PATCH /v1/projects/{project_id}/observability` | `projects:write` | member | `If-Match` |
| `PUT /v1/projects/{project_id}/scrub/{group}` | `privacy:write` | admin | approval |
| `PUT /v1/projects/{project_id}/checkin-auth` | `keys:manage` | member | approval |
| `PUT /v1/projects/{project_id}/environments/{name}/hidden` | `projects:write` | member | — |
| `DELETE /v1/projects/{project_id}` | `projects:delete` | admin | approval |
| `POST /v1/projects/{project_id}/restore` | `projects:write` | member | — |
| `POST /v1/projects/{project_id}/test-event` | `projects:write` | member | — |

### `releases`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/projects/{project}/releases` | `releases:read` | viewer | node data |
| `GET /v1/projects/{project}/releases/{version}/health` | `releases:read` | viewer | node data |

### `service_principals`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/service-accounts` | `tokens:manage` | admin | approval · `Idempotency-Key` |
| `PATCH /v1/service-accounts/{id}` | `tokens:manage` | admin | approval |

### `slos`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/slos` | `monitors:read` | viewer | paged |
| `GET /v1/slos/{slo_id}` | `monitors:read` | viewer | — |
| `POST /v1/slos` | `monitors:write` | member | `Idempotency-Key` |
| `PATCH /v1/slos/{slo_id}` | `monitors:write` | member | `If-Match` |
| `DELETE /v1/slos/{slo_id}` | `monitors:write` | member | `If-Match` |
| `PUT /v1/slos/{slo_id}/hold` | `monitors:write` | member | — |
| `POST /v1/slos/import` | `monitors:write` | member | — |

### `spans`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/spans` | `traces:read` | viewer | node data · `start`/`end` · paged |
| `GET /v1/spans/histogram` | `traces:read` | viewer | node data · `start`/`end` |

### `telemetry`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/telemetry/usage` | `usage:read` | viewer | node data |

### `tickets`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/tickets` | `issues:read` | viewer | node data |

### `tokens`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/tokens` | `tokens:manage` | admin | approval · `Idempotency-Key` |
| `POST /v1/service-accounts/{service_principal_id}/tokens` | `tokens:manage` | admin | approval · `Idempotency-Key` |
| `DELETE /v1/tokens/{id}` | `tokens:manage` | admin | — |
| `PUT /v1/token-policy` | `tokens:manage` | admin | approval |

### `traces`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/traces/{trace_id}` | `traces:read` | viewer | node data |
| `GET /v1/traces/{trace_id}/spans` | `traces:read` | viewer | node data |
| `GET /v1/traces/{trace_id}/spans/{span_id}` | `traces:read` | viewer | node data |

### `upload_tokens`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `POST /v1/projects/{project_id}/upload-tokens` | `keys:manage` | member | approval · `Idempotency-Key` |
| `DELETE /v1/upload-tokens/{token_id}` | `keys:manage` | member | approval |

### `uptime`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/uptime-checks` | `monitors:read` | viewer | paged |
| `GET /v1/uptime-checks/{check_id}` | `monitors:read` | viewer | — |
| `POST /v1/uptime-checks` | `monitors:write` | member | `Idempotency-Key` |
| `PATCH /v1/uptime-checks/{check_id}` | `monitors:write` | member | `If-Match` |
| `DELETE /v1/uptime-checks/{check_id}` | `monitors:write` | member | `If-Match` |
| `PUT /v1/uptime-checks/{check_id}/hold` | `monitors:write` | member | — |
| `POST /v1/uptime-checks/{check_id}/runs` | `monitors:write` | member | — |

### `usage`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/usage` | `usage:read` | viewer | — |

### `workspace`

| Call | Scope | Role | Notes |
|---|---|---|---|
| `GET /v1/workspace` | `org:read` | viewer | — |
