# REST API

> The concepts every doorway shares, your tenant id, the live OpenAPI schema, the endpoints most people want, pagination, the per-investigation stream and what REST lacks.

_Reference · Updated 2026-10-27 · Describes release 0.1.164_

Everything the portal does, it does over this API, and the token, scope and permission rules below hold on every doorway: the [MCP server](/help/mcp-server.md), the [agent API](/help/agent-api.md) and [aiopsctl](/help/aiopsctl.md). The token itself is created on [Personal access tokens](/help/personal-access-tokens.md).

## Concepts you need first {#concepts-you-need-first}

**A token acts as you.** A personal access token resolves to your user account, so your tenant role applies unchanged. A token can never do something your role cannot; the RBAC permission is checked on every single call, and a member who lacks it gets a 403 naming the exact permission key.

**Token scope and workspace role both apply.** Personal-token scopes are enforced on REST, the agent API and MCP. A `read` token cannot approve or remediate, even when its owner has those permissions. Use `write` for permitted operational changes and the agent API; credential management, changes to members, the workspace identity provider or an organization's workspace staffing (each gives another person a login), and platform REST administration routes require `admin` scope as well as the corresponding user permissions. Choose the smallest scope and role your integration needs. See [Members and roles](/help/members-and-roles.md) for workspace roles.

**An SSO access token works instead of a personal access token.** If your organization uses per-tenant SSO you may send your identity provider's access token as the bearer. It carries your **full** user scope on every doorway, so only your role limits it.

**Identifiers differ per doorway.** REST and the agent API take the tenant **id** in the path. Webhooks take the tenant **slug**. The own-agent setup uses `/mcp/aiops?workspace=<slug>` to select one workspace. Existing unbound MCP clients may pass a `tenant` slug explicitly; when omitted, the server defaults only if membership is unambiguous. Copy the ready-made URLs from the **Alert intake & tokens** page rather than assembling them by hand. This mismatch is the single most common integration bug.

**Unauthorized reads return 404, not 403.** If you are not a member of a tenant, its existence is hidden — platform staff included, there is no superadmin bypass. A tenant that has been disabled fails closed the same way. Missing a permission inside a tenant you *are* in returns 403. See the troubleshooting table at the end.

**The guardrails are identical on every doorway.** The write gate, the read-only x-ray guard, the lakehouse scrub-at-capture and the per-run cost cap are code, not per-integration settings. An MCP client cannot bypass the approval gate; the agent API cannot execute an unapproved write. On the shipped default the agent's write is refused mid-diagnosis and returned to you as a remediation plan; your platform operator can switch the fleet to pause mid-run instead. Either way no write reaches your estate without a named human (the one exception, off by default, repeats unchanged a fix a named approver certified: [Automatic API fixes for proven runbooks](/help/automatic-api-fixes.md)). The posture and the floors that produce it are owned by [Guardrails and cost limits](/help/guardrails-and-cost-limits.md#which-posture-your-deployment-runs).

Alert payload scrubbing at ingest is the one privacy chokepoint that is a platform switch (`AIOPS_PII_SCRUB_ALERTS`, off out of the box); the lakehouse chokepoint has no off switch.

## REST API {#rest-api}

Everything the portal does, it does over this API. There is no private back channel.

### Find your tenant id

REST paths carry the tenant **id**, not the slug. One call returns it along with everything else a client needs to know about you.

```bash
curl https://aiops.example.com/api/aiops/me \
  -H "Authorization: Bearer $PAT"
```

The response carries `user` and a `memberships[]` array. Each membership gives `tenant_id`, `slug`, `name`, `disabled`, your `role`, whether it is a temporary `support` grant, the full `perms[]` list your role resolves to, the `features[]` that are active for that tenant, and the tenant `brand`. Reading `perms[]` is the reliable way for a script to decide whether a call will be allowed before making it.

`GET /api/aiops` returns the tenant rows you are a member of (id, slug, name, disabled, settings, created_at) and is what `aiopsctl` uses to accept a slug wherever an id is expected. It is membership-scoped, not a platform-wide listing — platform staff see only their own memberships here too.

### The live schema is the endpoint reference

The platform serves its own OpenAPI document, and that is the reference to script against — a hand-maintained endpoint list in a handbook goes stale, the schema cannot.

| URL | What it is |
|---|---|
| `https://aiops.example.com/docs` | interactive schema browser for the whole API |
| `https://aiops.example.com/openapi.json` | the raw OpenAPI document, for client generators |

Note the collision: `/docs` is the API schema, while `/portal/docs` is this handbook.

### The endpoints most people want {#the-endpoints-most-people-want}

All of these are `GET`, all are tenant-scoped, and all obey the same 404-for-non-members rule.

| Endpoint | Query parameters | Permission |
|---|---|---|
| `/api/aiops/{tid}/incidents` | `status`, `limit` (default 100), `offset` | `incidents.view` |
| `/api/aiops/{tid}/incidents/{incident_id}` | — | `incidents.view` |
| `/api/aiops/{tid}/alerts` | `status`, `limit` (default 100), `offset` | `incidents.view` |
| `/api/aiops/{tid}/investigations` | `limit` (default 200), `offset` | `incidents.view` |
| `/api/aiops/{tid}/artifacts` | `source_type`, `source_id`, `kind` | `incidents.view` |
| `/api/aiops/{tid}/feedback` | `limit` (default 200), `offset` | `report.view` |

`status` is validated against the enum, so a typo is a `422` with the accepted values, not a `400`. Incidents accept `open`, `investigating`, `awaiting_approval`, `remediating`, `resolved`, `escalated`; alerts accept `received`, `correlated`, `suppressed`, `ignored`.

Three behaviors to design around:

- **The incident detail endpoint is the fat one.** `/incidents/{id}` returns the incident plus its `alerts[]`, `investigations[]`, `approvals[]` and the 25 newest `rechecks[]` in a single response. There is no separate approvals-by-incident endpoint; you read them from here.
- **The investigations list hides chat and x-ray runs**, and the detail endpoint `/investigations/{id}` returns 404 for them. That is deliberate: a chat turn carries another member's transcript, and x-ray phases are estate-scan internals reachable only through the x-ray endpoints. Do not treat the gap as missing data.
- **Feedback is gated on `report.view`, not `feedback.submit`.** Submitting a score needs `feedback.submit`; reading the aggregate needs `report.view`. A role with one and not the other is a common cause of a surprising 403.

The mutations worth scripting are the same ones the portal exposes as buttons:

| Endpoint | Body | Permission |
|---|---|---|
| `POST /api/aiops/{tid}/investigations/{inv_id}/decisions` | an **array** of `{approval_id, decision, feedback, edited_command}` | `approvals.decide` |
| `POST /api/aiops/{tid}/investigations/{inv_id}/feedback` | `{rating, score, correction}` | `feedback.submit` |
| `POST /api/aiops/{tid}/incidents/{id}/escalate` | optional `{assignee}` | `incidents.view` |
| `POST /api/aiops/{tid}/incidents/{id}/resolve` | optional `{note}` | `incidents.act` |
| `POST /api/aiops/{tid}/incidents/{id}/remediate` | — | `incidents.act` |
| `POST /api/aiops/{tid}/incidents/{id}/rca-export` | — | `incidents.act` |
| `POST /api/aiops/{tid}/concerns` | `{summary, detail, service, severity}` | `incidents.act` |

Escalation is the andon cord, so any member who can see an incident may pull it, viewer included; the audit row records who. Everything that writes to your estate or burns tokens sits behind `incidents.act`.

The decisions endpoint takes an array so a batch of approvals on one investigation is decided in a single transaction. Every element must belong to that investigation, `edited` must carry `edited_command` (400 otherwise), and the first decision wins — a re-decide is `409 approval already decided: <decision>`.

Workspace settings that have no portal editor — `aiops_storm_group_window_s`, `aiops_storm_spread_window_s`, `aiops_grouping_wait_s`, `aiops_storm_agent`, `label_map`, `severity_map`, `criticality_map`, `change_resource_map`, `aiops_change_harvest_interval_s`, `aiops_topology_fold_origins`, `aiops_require_identity_labels`, `assignment_map`, `change_freeze`, `aiops_auto_rollback`, `aiops_verify_external_changes` and the last-known-good keys below — are set with `PATCH /api/aiops/{tid}` and a body such as `{"settings": {"aiops_storm_agent": false}}`, which needs `members.manage`. Each key sent replaces that key whole and the others are kept. Eight are checked when saved, and a value the platform cannot use is refused with a 400 that names the key: the two grouping windows and the grouping wait must be whole seconds, the group window `0`–`3600`, the spread window `0`–`7200` and the wait `0`–`600` (a stored `aiops_storm_threshold` from before 0.1.147 is accepted and ignored); `criticality_map` levels known words; each `change_resource_map` pattern must name something (see *Linking captured changes to your services* in the *Datasource configuration guide* in the delivered documentation); and the three switches `aiops_lkg_cause_block` (the ranked changes in the agent's prompt) and `aiops_lkg_record` (the last-known-good record), both on by default, and `aiops_require_identity_labels` (off by default; JSON `true` holds an alert that carries neither a `service`/`app`/`deployment`/`namespace` label nor an `alertname`: its incident opens with no analysis, "Held for triage — missing identity labels", and the source's clear still closes it) take JSON `true` or `false` only — `null` restores the default, and a string such as `"false"` or `"off"` is refused rather than read two ways, through the API and the MCP `patch_tenant` tool alike. `label_map` and `severity_map` are not checked when saved: send JSON objects — `{"source_key": "canonical_key"}` and `{"source_value": "canonical"}` — because another shape fails every alert delivery to the workspace. The rest are read defensively, never refused: `aiops_topology_fold_origins` (the dependency sources allowed to fold alerts, `["declared"]` by default; a list replaces the default whole, so keep `declared` in it, and an unknown name is ignored — see [Service dependencies](/help/mission-overview.md#service-dependencies)), `aiops_change_harvest_interval_s` (seconds, 900 by default; `0` stops the scheduled audit-log harvest and the pull when an incident opens), `aiops_lkg_lookback_h` (hours, 72 by default, held to 1–168) and `aiops_lkg_suspect_h` (hours, 2 by default, held to 1–24). A number that cannot be read falls back to the default. The remaining keys take these shapes (release windows are now validated on save; the legacy freeze/assignment keys are not): `assignment_map` (`{"<service glob>": "<assignee>"}`) names the owner a new incident opens with: the first pattern that matches its alert's `service` label (its `namespace` when the alert has no service). `change_window` and `change_freeze` take `{"days": ["sat"], "hours": "22:00-06:00"}` in UTC, where an overnight range wraps midnight but the day is checked on its own — that example is Saturday 00:00–06:00 and 22:00–24:00, so add `"sun"` for the night into Sunday: the first is when a person may release an L2 or L3 change, the second when a change observed outside the platform is flagged unauthorized (see [Change windows, emergency changes and rollback](/help/change-control.md)). `aiops_auto_rollback` (JSON `true`) lets the rollback sweep run an operator-declared rollback script for a failed L1 or L2 change, and `aiops_verify_external_changes` (JSON `true`) checks a captured external change against its dependents, advisory only; both are off by default. The notification interval has its own field under **Settings · Account → Notifications & on-call**. The reopen window (`aiops_reopen_window_h`, whole hours `0`–`8760`), the unmapped-signal triage switch (`aiops_unmapped_triage`, JSON `true` or `false`) and the flap threshold (`aiops_flap_threshold`, whole number `0`–`100`) have theirs under **Settings · Data in → Alert intake & tokens → Intake rules**; the same keys sent through `PATCH /api/aiops/{tid}` are checked the same way (see [Unmapped signals](/help/storms-and-unmapped-signals.md#unmapped-signals) and [Flapping incidents](/help/storms-and-unmapped-signals.md#flapping-incidents)). The one-click source mapping on an unmapped incident (the **Unidentified service — map this source** card) never touches `label_map`: it adds the label to `source_service_labels`, `{"<source>": ["<label>", …]}` per webhook source (`generic`, `cloudwatch-sns`, …), which names the service of a later alert from that source only when the alert carries no `service` — the label stays and an alert's own service wins. The same card lists and removes the mapped sources; through `PATCH /api/aiops/{tid}` the key is checked when saved (each source a list of label names).

Maintenance windows are not API-only: use the **Maintenance windows** card or `PATCH /api/aiops/{tid}/guardrails` for the workspace default `change_window` and ordered service map `change_windows`; `PATCH /api/aiops/{tid}` remains supported for compatibility. A one-time exception is requested and decided on the individual change through the two `window-exception` routes described in the OpenAPI schema.

`aiops_cross_cloud_plans` is set the same way and is off by default. It takes JSON `true` or `false` only — `null` restores the default, and any other value is refused with a 400 that names the key. On, a prepared fix may act on another cloud than the one its incident came from: the step stays reviewable with a warning the approval binds, instead of being refused (see [Review and apply a fix](/help/approve-and-run-a-fix.md#review-and-apply-a-fix)). A save that flips it, or any of the JSON `true`/`false` switches above, names each flipped switch with its old and new value in its audit row (*Workspace settings changed*), so who allowed fixes across clouds, and when, stays on record.

### Pagination

List endpoints return a plain JSON array and put the unfiltered total in a response header.

- `limit` is clamped to **1–500** regardless of what you send.
- `offset` is clamped to a minimum of 0.
- **`X-Total-Count` carries the total** over the same filter, not the length of the page. Page by reading that header, never by "keep going until a short page".

`GET /{tid}/artifacts` is the exception: it takes no `limit` or `offset` and returns the newest **100** rows only. Older artifacts are reachable by id at `/{tid}/artifacts/{artifact_id}`. If you archive artifacts, poll and store them rather than expecting to page backwards.

If your client is a browser page on a different origin, `X-Total-Count` is only readable when your platform operator has configured the split-frontend origin; otherwise the browser hides the header even though the server sent it. Server-side clients always see it.

### Watch one investigation live

There is a server-push channel, and it is per-investigation: `GET /api/aiops/{tid}/investigations/{inv_id}/stream` is a Server-Sent Events endpoint behind `incidents.view`. It is what the incident page uses, and it is the honest way to follow a run without polling.

1. Open the stream with your token as the bearer.
2. Read the first `snapshot` event: `status`, `pillar`, `rca_markdown`, `error`.
3. Read the replayed `tool` events — one per tool call already persisted — so a late subscriber still gets the whole timeline.
4. If an approval is pending, a single `approval` event carries the waiting rows.
5. Stay connected for live `tool` events; a `ping` arrives every 15 seconds of silence.
6. Stop on the `end` event, which carries the terminal `status`.

You should see the timeline reach `end` with `status` `done` or `failed`. Event ids are the tool-record index, so reconnecting with `Last-Event-ID` (or the `?after=` query parameter) resumes exactly where you left off without duplicating or dropping. The `approval` and `ping` events deliberately carry no id, so they never advance your cursor. Chat and x-ray investigations return 404 here, same as on the detail endpoint.

### Cross-origin mutations are refused

The API rejects `POST`, `PATCH`, `PUT` and `DELETE` that arrive with an `Origin` header naming a host other than the API's own, unless your operator has allowlisted that origin. The rejection is `403 {"detail": "cross-origin not allowed"}`. This is the CSRF layer, and it applies to token-bearing requests too. Requests with no `Origin` header — curl, server-to-server, CI — always pass. If you are building a browser front end against this API, ask your platform operator to add its origin.

### What REST does not have {#what-rest-does-not-have}

Say it plainly so you do not go looking:

- **Pending tool approvals are available in one call:** `GET /api/aiops/{tenant_id}/pending-approvals` or the MCP tool `list_pending_approvals`. Both exclude private chat, Discovery and benchmark sources. Every decision waiting on a person (incidents awaiting approval, escalated incidents, change approvals, merge proposals, risk requests, post-implementation reviews and routes a rollback demoted) is one call too: `GET /api/aiops/{tenant_id}/needs-you` or the MCP tool `needs_you`.
- **There is no tenant-wide event stream.** The SSE endpoint above is scoped to one investigation id; there is no firehose to subscribe to for "anything that needs me".
- **Outbound notifications exist for incident open, escalate and resolve** (Slack, Teams, webhook, email with on-call resolution, and the PagerDuty and Opsgenie pagers) under *Settings · Account → Notifications & on-call*. Chat, email and webhook targets can also take `golden` (the daily agent check failed), `rollback` (a change failed, and what happened to its rollback), `image_not_deployed` (the scanner still reports the old digest of a rebuilt image; repeated every `aiops_image_reminder_h` hours until it stops — delete or untag the old digest once nothing runs it) and `change_schedule` (a compliance change scheduled to run later, then released or not run at its time, or cancelled). A PagerDuty target sends Events API v2 `trigger` and `resolve` events with the incident id as the `dedup_key`; an Opsgenie target creates an alert with the incident id as its `alias` and closes it by alias. So one incident is one page however many alerts join it, and the escalation re-triggers the same page. The page follows the incident: every close resolves it — including a bulk close, a merge, a storm split, a risk acceptance and a passing control, which send no chat message — and a reopen triggers it again (audited as event `reopen`); chat and webhook targets get nothing on a reopen. The routing key (PagerDuty's Events API v2 integration key) or API key (Opsgenie) is write-only: it is kept in the workspace secret store, the page shows only that a key is stored, and the events go to the vendor's fixed US or EU endpoint, never a URL you type. Pagers are never grouped into a digest and receive incident events only. They do not fire on the approval gate — for that see *Wire pending approvals into your own paging*. Bursts are grouped per target: a Slack, Teams or email target gets at most one message per interval — 120 seconds unless the workspace setting `aiops_notify_min_interval_s` (0–3600, `0` = send every event) says otherwise. A workspace owner sets it in **Message interval (seconds)** under **Settings · Account → Notifications & on-call** (empty = the platform default). The first event goes out at once; later ones are held until the interval ends and then sent as one digest (a single held event goes out as a normal message), P1 first and escalations first within each priority, at most 20 lines plus a count. While a digest is pending, every newer event for that target joins it, so nothing overtakes a held message. Each control-plane replica keeps its own interval, so a deployment scaled to several replicas can send one message per replica. Webhook targets are never grouped: each event arrives as its own JSON.
- **There is no webhook-token read endpoint.** The token is shown once at minting and only its hash is stored.

## Troubleshooting {#troubleshooting}

| Symptom | What it means | What to do |
|---|---|---|
| `404 tenant not found` on a tenant you know exists | you are not a member, your membership expired, or the tenant is disabled — existence is deliberately hidden | ask an owner for membership; there is no superadmin bypass |
| `403 requires permission 'x' on this tenant` | you are a member; your role lacks that key | read `perms[]` from `/api/aiops/me` and compare, then ask for the role that carries it |
| `404` on `/investigations/{id}` for a run you can see elsewhere | it is a chat or x-ray investigation, hidden on this endpoint by design | read it through `/chat/{sid}` or the x-ray endpoints instead |
| `409 already decided: approved` | someone decided it first | re-read the approval; the first decision is final |
| `422` on a list call with `status=` | the value is not in the enum | use the exact values listed above |
| `403 cross-origin not allowed` on a mutation | a browser sent an `Origin` the API does not allowlist | call server-side, or have your operator allowlist the front end's origin |
| `X-Total-Count` missing in a browser client | cross-origin, and the header is not exposed to that origin | read it server-side, or have your operator configure the split-frontend origin |

## Next steps {#next-steps}

- [Personal access tokens](/help/personal-access-tokens.md) — create, scope, rotate and revoke the credential every call above uses.
- [MCP server](/help/mcp-server.md) and [Agent API (OpenAI-compatible)](/help/agent-api.md) — the same operations from an MCP client or an OpenAI SDK.
- [Settings reference](/help/settings-reference.md) — every workspace setting `PATCH /api/aiops/{tenant_id}` and `/guardrails` accept.
- [Status and error reference](/help/status-and-error-reference.md) — the status words and HTTP errors the endpoints answer with.
