Escalation Policies — API v1 & MCP | Nodea
An escalation policy is an ordered list of steps that decide who gets notified, and after what delay, during an incident. Each step targets a single notification route — a notification group or an integration — plus a delay counted from incident open. This document covers the REST API v1 operations and the full set of MCP tools for managing policies.
All endpoints live under the base path https://app.nodea.io/api/v1. Authenticate with a Passport personal access token in the Authorization: Bearer <token> header. The throttle:api-v1 limit of 240 requests per minute applies. Escalation policies require the Websites module (Pennant flag websites) — the routes ride the same flag as website monitoring, they do not have a dedicated one.
Access is owner-scoped: a foreign or unknown id always returns 404 (not 403), so the API never confirms that another tenant's resource exists. OAuth scopes: reading requires alerting:read, writing alerting:write, deleting alerting:delete.
API v1
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /escalation-policies | alerting:read | List the owner's policies (pagination, search, sorting). |
| GET | /escalation-policies/{escalationPolicy} | alerting:read | A single policy with its steps in position order. |
| POST | /escalation-policies | alerting:write | Create a policy with a list of steps (returns 201). |
| PATCH / PUT | /escalation-policies/{escalationPolicy} | alerting:write | Update the name and/or the whole step chain. |
| DELETE | /escalation-policies/{escalationPolicy} | alerting:delete | Permanently delete a policy and its steps (returns 204). |
The step object (steps[]) and the XOR rule
The steps field is an array of steps. Each step carries:
delay_minutes— integer0–1440. The delay counted from incident open (not from the previous step) after which this step fires. A value of0means notify immediately.notification_group_id— either the id of one of your notification groups (fans out to every integration in the group).integration_id— or the id of a single one of your integrations.
The XOR rule: each step must target exactly one of the two — notification_group_id OR integration_id. Two validation layers enforce this:
- The neither target case (both omitted) — a
required_withoutrule on both fields returns422. - The both targets case — an additional
withValidatorhook adds an error to thesteps.{index}key and returns422with a message that a step must target one route, not both.
Every step target must belong to the token user. Via a Rule::exists(...)->where('user_id', ...) constraint, a foreign group or integration id also returns 422 (not 404) — so an escalation can never route through another tenant's credentials.
Position and ordering
The position field is not accepted in the request — the array order of steps becomes the 0-based position (first element = 0, second = 1, …). That order defines the escalation chain. In the response, steps are always returned sorted by position.
Response shape
The EscalationPolicyResource returns a policy with nested steps:
{
"data": {
"id": 42,
"name": "Critical production API",
"steps": [
{
"id": 101,
"position": 0,
"delay_minutes": 0,
"notification_group_id": 7,
"integration_id": null
},
{
"id": 102,
"position": 1,
"delay_minutes": 15,
"notification_group_id": null,
"integration_id": 3
}
],
"steps_count": 2,
"created_at": "2026-07-15T09:00:00+00:00",
"updated_at": "2026-07-15T09:00:00+00:00"
}
}In each step exactly one of notification_group_id / integration_id is populated, the other is null. The steps_count field appears when the controller counted the relation.
GET /escalation-policies — list parameters
Supported query parameters: search (name substring, max 255), sort (whitelist: name, created_at), direction (asc / desc), per_page (1–100, default 20), page. Default sort: alphabetical by name.
curl -s "https://app.nodea.io/api/v1/escalation-policies?search=API&sort=name&per_page=20" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"POST /escalation-policies — create with steps
curl -s -X POST "https://app.nodea.io/api/v1/escalation-policies" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"name": "Critical production API",
"steps": [
{ "delay_minutes": 0, "notification_group_id": 7 },
{ "delay_minutes": 15, "integration_id": 3 }
]
}'The first step (position 0) fans out immediately to group 7; the second (position 1) hits integration 3 after 15 minutes. Response: 201 Created with the full resource.
PATCH /escalation-policies/{id} — update
Updates are partial: name and steps are optional. The name only changes when the name key is sent. The steps list has replace-all semantics — the controller deletes the existing steps and recreates them from the submitted array in its order, but only when the steps key was present. Omitting steps keeps the existing chain.
# Rename only (steps unchanged)
curl -s -X PATCH "https://app.nodea.io/api/v1/escalation-policies/42" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Critical API — renamed" }'DELETE /escalation-policies/{id}
Permanently deletes the policy and its steps, returning 204 No Content. Websites pinned to the policy do not block the delete — their websites.escalation_policy_id key is nulled by the database (nullOnDelete) and they fall back to the default "notify all channels on first failure" behaviour.
curl -s -X DELETE "https://app.nodea.io/api/v1/escalation-policies/42" \
-H "Authorization: Bearer $TOKEN"Error codes
| Code | Meaning |
|---|---|
401 | Missing or invalid Bearer token. |
403 | Token without the required scope (alerting:read / write / delete) or the websites module disabled. |
404 | The policy does not exist or belongs to another user. |
422 | Validation error: missing name/steps, delay_minutes out of 0–1440, XOR violation (neither or both targets), or a target outside your own groups/integrations. |
MCP
The same operations are exposed by 5 MCP tools. They enforce identical scopes and require the websites module. In the tools, policy ids are passed as strings. The delete tool is marked destructive and requires an explicit confirm: true.
| Name | Scope | Mode | Description |
|---|---|---|---|
escalation-policies-list | alerting:read | read-only | Paginated list of policies with steps; parameters search, page, per_page (max 50). |
escalation-policy-get | alerting:read | read-only | A single policy with steps by id; adds group and integration names. |
escalation-policy-create | alerting:write | write | Create a policy from name and steps (list order = escalation order). |
escalation-policy-update | alerting:write | write (idempotent) | Update name and/or steps; a provided steps list replaces the whole chain. |
escalation-policy-delete | alerting:delete | destructive | Permanent delete; requires confirm: true. |
Passing steps
In escalation-policy-create and escalation-policy-update, the steps parameter is an array of objects with delay_minutes (0–1440), notification_group_id, and integration_id — with the same XOR rule (exactly one target per step). The element order becomes position. Targets outside your own groups/integrations are rejected (a single foreign id aborts the whole operation).
tools/call — create
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "escalation-policy-create",
"arguments": {
"name": "Critical production API",
"steps": [
{ "delay_minutes": 0, "notification_group_id": 7 },
{ "delay_minutes": 15, "integration_id": 3 }
]
}
}
}tools/call — delete (with confirmation)
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "escalation-policy-delete",
"arguments": {
"id": "42",
"confirm": true
}
}
}Without confirm: true the tool returns the error "Deletion requires confirm: true." and deletes nothing. On success it returns { "deleted": true, "id": "42" }. The tools return structured data — the policy with its steps in position order, together with group names (notification_group_name) and integration names (integration_name).
