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, the agent API and aiopsctl. The token itself is created on Personal access tokens.

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 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). The posture and the floors that produce it are owned by Guardrails and cost limits.

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

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.

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

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 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), 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). 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 and 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). 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.

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

Say it plainly so you do not go looking:

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