# REST API

> The API at a glance — auth, errors, pagination, and the live stream.

Web page: https://infrainbox.app/docs/reference/rest-api/

Everything lives under `/v1` on your instance's own address, for example `https://infrainbox.example.com/v1`.

## The spec itself

The full OpenAPI document is served live, no credential needed, at `GET /v1/openapi.json`. It's always in sync with the running server — generate a client from it, or just read it.

## Authentication

| Credential | Who uses it | Form |
|---|---|---|
| Session cookie | The web dashboard | Set by sign-in; sent automatically by the browser. |
| Device bearer token | A paired phone app | `Authorization: Bearer <token>`, opaque, refreshed automatically. |
| Source key | Something sending events | `Authorization: Bearer iik_src_…`, on `POST /v1/events`, a tool's own webhook path, and the watcher's own calls (enrolling its heartbeat, `POST /v1/watchers/check-in`). |
| Heartbeat token | A heartbeat ping | `Authorization: Bearer iik_hb_…`, or as the ping URL's path segment. |

Signing in through an identity provider uses three endpoints: `GET /v1/auth/providers` lists the configured providers, `POST /v1/auth/oidc/{id}/start` begins a sign-in, and `GET /v1/auth/oidc/{id}/callback` finishes it. The start answers with the provider's authorization URL (`authorizeUrl`) and sets a short-lived cookie that ties the sign-in to the browser, which the dashboard then sends to that URL; the callback always answers with a redirect (303) back into the dashboard. They are steps of a browser flow, not calls for a script. `{id}` is the provider's id from [the configuration](https://infrainbox.app/docs/self-hosting/configuration.md#sign-in-with-an-identity-provider).

The account links a server mails out (see [Your account](https://infrainbox.app/docs/start/account.md)) each have a public endpoint that takes the link's token in the JSON body: `POST /v1/auth/password-reset` and `/password-reset/confirm`, `POST /v1/auth/email-change/confirm`, and on InfraInbox Cloud `POST /v1/auth/signup` and `/verify-email`. They exist only where `GET /v1/info` lists the matching capability (`auth.password_reset`, `mail`, `auth.signup`), and answer 404 elsewhere. A refused link is always the same 403 `token_invalid`, whatever was wrong with it.

Workspaces and their people sit under `/v1/workspaces`: `POST /v1/workspaces` creates one with you as its owner, `GET|PATCH /v1/workspaces/{id}` reads and changes its settings, `GET /v1/workspaces/{id}/members` lists who is in it, and `PATCH|DELETE /v1/workspaces/{id}/members/{userId}` changes a role or removes somebody. Invitations are `GET|POST /v1/workspaces/{id}/invites` and `DELETE /v1/workspaces/{id}/invites/{inviteId}`; the reply to a new invitation carries its link once and never again. The invited person accepts with `POST /v1/invites/accept`, which takes the link's token in the body. Reading is open to every member of the workspace; everything that changes a membership is for owners only, behind a recent step-up, and a refused invitation is always the same 403 `invite_invalid`. See [Workspaces and members](https://infrainbox.app/docs/start/workspaces.md).

> **Note:** There's no separate personal-access-token yet for scripting against the management API directly. What the dashboard and the mobile app call — incidents, sources, rules, destinations, settings — is the same API described here, but it's the app's own API rather than a stable, versioned surface for third-party automation. `POST /v1/events` (a source key) is the one endpoint meant for that from day one.

## Errors

Every error response is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details, `application/problem+json`:

```json
{
  "type": "https://infrainbox.app/problems/validation_failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "1 field is invalid.",
  "code": "validation_failed",
  "requestId": "b6f1c2d4e8a94f3b",
  "errors": [{"pointer": "/severity", "detail": "must be one of DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL"}]
}
```

`code` is the stable, machine-readable reason; `type` is always `https://infrainbox.app/problems/<code>`; `requestId` matches what the server logged, for support. A validation failure (422 `validation_failed`) lists one `errors` entry per bad field, each with a JSON pointer and a plain-language reason.

## Deleting and exporting

Three operations are destructive or export everything, so they all need a recent re-authentication (a 403 with `code: reauth_required` says so, and lists the factors the account can prove) and all three are written to the audit log:

| Operation | What it does |
|---|---|
| `DELETE /v1/me` | Closes the caller's own account, and with it every workspace the account owned alone. 409 `workspace_needs_owner` names a workspace that has other members; 409 `last_instance_admin` refuses the only administrator. |
| `DELETE /v1/workspaces/{id}` | Closes one workspace, owners only. 409 `last_workspace` refuses the account's only one. |
| `GET /v1/workspaces/{id}/export` | Streams the workspace as one JSON document (owners only), as a file attachment. It holds no secret — no key, key hash or sealed credential. One export per workspace per minute; a second answers 429. |

Both deletions end access with the answer and leave the rows to the background purge (see [Security and privacy](https://infrainbox.app/docs/self-hosting/security.md)).

## Destinations

`GET /v1/destinations` and `GET /v1/destinations/{id}` also say, per destination, `usedBy` — whether the default route sends there, and the rules and routing profiles that do (up to 20 of each by ID and name, plus the totals) — and `lastDelivery`: the last message that was sent or failed for good, when, and a sanitized reason if it failed (`status: none` before the first one).

## Pagination and sorting

List endpoints support two paging styles:

- **`limit`** (1–200, default 50) with **`cursor`** — keyset paging: pass the previous page's `nextCursor` to get the next one. Best for a live feed.
- **`limit`** with **`offset`** — numbered paging (page `n` is `offset = (n-1) * limit`); the response carries `total`, so a table can say "1–50 of 312". `cursor` and `offset` are mutually exclusive.

Most list endpoints also take `sort` and `order` (`asc`/`desc`, default `desc`) — the exact sort columns vary by endpoint (events sort by `received`, `occurred`, `severity`, `source`, `title` or `disposition`, for example).

## The live stream

`GET /v1/stream` (session cookie or device bearer) is a Server-Sent Events stream of what changed in your workspaces — a `data:` line per change, so a client refetches the specific resource that changed within about a second instead of polling. Every event carries a reference only, never state. On connect, and whenever the server's own change feed breaks, it sends `resync`, meaning "reload everything you show." A comment heartbeat (`: ping`) arrives every 25 seconds so an idle connection isn't mistaken for a dead one; a client that falls behind is disconnected and reconnects to a fresh `resync`.
