Alertingv7.0.20
The modern, class-based alert engine — alerts, classes, outlets, routing rules, templates, and maintenance windows — together with the older per-device legacy alert endpoints and the shared alert-history ledger that records everything that fires.
The appliance has a modern, class-based alert engine. An alert watches a stream of log data — system logs (syslog_log), Windows event logs (event_log), or intrusion-detection events (eve_log) — and fires when matches within a lookback window cross a threshold. Each alert carries a severity (1–5) and a class-specific set of match parameters. Alerts say what to watch; they do not say who to notify.
Delivery is handled by a separate routing layer. Outlets are the destinations (email, webhook, Slack, Teams, Discord). Routing rules decide which outlets receive which events, filtered by alert class, severity, device tags, and event-key patterns. Outlet templates shape the JSON payload sent to webhook-style outlets, and email templates shape the notification emails. Maintenance windows suppress delivery on a schedule. This page also exposes a read-only catalog of the older per-device legacy handlers, used when a routing rule is scoped to a legacy alert (legacy alert definitions and history are covered under Legacy alerts at the end of this page).
These endpoints belong to the alerts functional area; an MCP or OAuth token needs the mcp:alerts scope, and the calling user must hold the alerts permission shown on each endpoint. Read and write share the same permission — a user who can view alerts can also change them. The only finer-grained restriction is tag-scoping: routing rules and the legacy-handler catalog are filtered to the tags the calling user is restricted to (a user with no tag restriction sees everything; sa bypasses). Modern alerts are readable by any user holding the alerts permission, but a tag-restricted user may only scope an alert to a device or tag inside their own tag set — an out-of-scope scope write is rejected with 403. Outlets, outlet templates, and email templates carry no tag anchor at all — they are shared infrastructure.
Response shapes are not uniform across this page. Most of the modern resources return the bare model (or a bare array of models). Errors mostly return {"error": "..."}, but validation failures from the modern CRUD endpoints return {"errors": {...}} — and some with status 400, some with 422. Each endpoint below shows its real shape; do not assume a wrapper.
Alerts
Modern alert definitions live in this section: list, read, create, update, delete, and a one-shot enable/disable toggle. An alert’s class is fixed at creation time — it determines the evaluator and the shape of the parameters object — and cannot be changed afterward.
List alerts
GET/api/alertsalerts
Returns every modern alert definition, ordered by id. Not tag-scoped — modern alerts are class-wide and visible to any user with the alerts permission.
Response 200
[
{
"id": 12,
"label": "SSH auth failures",
"class": "syslog_log",
"severity": 2,
"parameters": { "facility": 4, "contains": ["Failed password"], "occurrences_min": 5, "lookback_minutes": 5 },
"enabled": true,
"scope": "global",
"scope_id": null,
"tag_filters": [],
"renotify_minutes": 0,
"throttle_flip_count": 0,
"throttle_flip_window_minutes": 5,
"throttle_stable_minutes": 5,
"last_evaluated_at": "2026-06-15T11:00:00Z",
"last_result_count": 0
}
]
Example
curl -sS https://APPLIANCE/api/alerts \
-H "Authorization: Bearer $TOKEN"
Get one alert
GET/api/alerts/{id}alerts
Returns a single alert by id.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Alert id. |
Response 200 — the bare alert object (same shape as a list element).
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Alert not found"} | No alert with that id. |
Create an alert
POST/api/alertsalerts
Creates a modern alert. The class is required and locked in for the life of the alert. The parameters object is validated against the chosen class’s schema — fetch that schema from List alert classes to know which keys a class accepts. Unknown classes fall back to accepting any parameters array.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | yes | Max 255. |
class | string | yes | One of syslog_log, event_log, eve_log. |
severity | integer | yes | 1–5. |
parameters | object | conditional | Class-specific match parameters; validated per class. Most classes require occurrences_min and lookback_minutes. |
enabled | boolean | no | Defaults to the column default. |
scope | string | conditional | global (the default), device, or tag. Required whenever scope_id is sent. |
scope_id | string | conditional | Omit or null for global; a devices.id for device (existence-checked); a tag slug for tag. An empty string is stored as null. |
tag_filters | string[] | no | Tag slugs, AND-semantics — the alert fires only on devices carrying every listed slug. Defaults to [] (no narrowing). |
renotify_minutes | integer | no | Minutes between reminder notifications; 0/null disables. |
throttle_flip_count | integer | no | Flip-suppression count. |
throttle_flip_window_minutes | integer | no | Flip-suppression window. |
throttle_stable_minutes | integer | no | Stable-period minutes. |
scope / scope_id / tag_filters carry the same axes as a routing rule but answer a different question: which devices this alert fires on. The evaluator applies them as a device filter before the match is counted, so an out-of-scope device opens no incident, writes no history row, and consults no route. scope=global with an empty tag_filters is the whole fleet — the default every pre-existing alert carries.
A tag-restricted caller may only scope an alert to a device or tag inside their own tag set; otherwise the write is rejected with 403 {"error":"You can only scope an alert to a device within your assigned tags."} (the message names the offending axis). scope=global stays writable by everyone. Reads are never tag-filtered.
Modern log-stream alerts are one-fire, edge-triggered events: each occurrence notifies once, and no reminder or recovery notification is ever sent. The renotify_minutes and throttle_* fields are accepted and stored for compatibility but do not affect delivery.
Response 201 — the created alert object.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed (note: 400, not 422). |
Example
curl -sS -X POST https://APPLIANCE/api/alerts \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"label": "SSH auth failures",
"class": "syslog_log",
"severity": 2,
"parameters": {
"facility": 4,
"contains": ["Failed password"],
"combine_op": "OR",
"occurrences_min": 5,
"lookback_minutes": 5
},
"enabled": true
}'
Update an alert
PUT/api/alerts/{id}alerts
Updates an alert. The class is immutable — the appliance validates parameters against the alert’s existing class and ignores any class value in the body. All fields are optional on update; supply only what you want to change.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Alert id. |
Request body — same fields as create, all optional. A class in the body is ignored.
Response 200 — the updated alert object.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
404 | {"error":"Alert not found"} | No alert with that id. |
Delete an alert
DELETE/api/alerts/{id}alerts
Permanently deletes an alert definition.
This removes the alert definition. The alert stops evaluating immediately.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Alert id. |
Response 200
{ "deleted": true }
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Alert not found"} | No alert with that id. |
Toggle an alert
POST/api/alerts/{id}/togglealerts
Flips the alert’s enabled flag — enables a disabled alert and vice versa. Takes no body.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Alert id. |
Response 200 — the alert object with its new enabled value.
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Alert not found"} | No alert with that id. |
Example
curl -sS -X POST https://APPLIANCE/api/alerts/12/toggle \
-H "Authorization: Bearer $TOKEN"
Alert classes
The class catalog drives the alert-creation wizard. Each class describes its parameter schema (with UI hints, options, and defaults) and the template variables its events expose.
List alert classes
GET/api/alert-classesalerts
Returns the three modern alert classes and everything needed to build a parameter form for each: the human label, a description, the full parameters schema, the {$variable} legend, and the id of the canonical email template for the class (resolved by label; may be null if that template row was removed).
Response 200
[
{
"name": "syslog_log",
"label": "Syslog",
"description": "Match syslog events by facility, severity, and message substrings. …",
"parameters": {
"facility": { "label": "Facility", "type": "select", "required": false, "options": [0,1,2,"…"], "optionLabels": ["kern","user","…"] },
"severity_ceiling": { "label": "Minimum Severity", "type": "select", "options": [0,1,2,3,4,5,6,7] },
"contains": { "label": "Match Substrings", "type": "string[]" },
"combine_op": { "label": "Combine Substrings", "type": "select", "options": ["AND","OR"], "default": "AND" },
"invert": { "label": "Invert Match", "type": "bool", "default": false },
"occurrences_min": { "label": "Minimum Occurrences", "type": "number", "required": true, "min": 1, "default": 1 },
"lookback_minutes": { "label": "Lookback Window (minutes)", "type": "number", "required": true, "min": 1, "max": 10080, "default": 5 }
},
"variables": [
{ "slug": "{$label}", "label": "Device Label" },
{ "slug": "{$message}", "label": "Syslog Message" }
],
"default_email_template_id": 3,
"default_email_template_label": "Syslog"
}
]
Example
curl -sS https://APPLIANCE/api/alert-classes \
-H "Authorization: Bearer $TOKEN"
Email templates
Email templates are the notification bodies the appliance sends for an alert. Each row carries a label, an optional class slug, an HTML body (email_template) and a plain-text alternate (alt_template), with {$variable} substitutions matching the alert class’s legend. Seeded “canonical” rows can be restored to their factory content.
List email templates
GET/api/email-templatesalerts
Returns all email templates, ordered by id.
Response 200
[
{
"id": 3,
"label": "Syslog",
"description": "Default body for syslog alerts.",
"class": "syslog_log",
"email_template": "<html>… {$message} …</html>",
"alt_template": "Device {$label} … {$message}"
}
]
List templates for the wizard picker
GET/api/alerts/templatesalerts
Returns the full email-template catalog for the alert-creation wizard’s picker. This reads the same rows as List email templates; it exists as a separate, wizard-facing route. Takes no parameters.
Response 200 — a bare array of email-template objects (same shape as List email templates).
Example
curl -sS https://APPLIANCE/api/alerts/templates \
-H "Authorization: Bearer $TOKEN"
List template classes
GET/api/email-templates/classesalerts
Returns the class slugs a template may carry — the three modern classes plus legacy_device_down — each with its {$variable} legend and a restorable flag indicating whether a canonical default exists for that class.
Response 200
[
{
"name": "syslog_log",
"label": "Syslog",
"variables": [ { "slug": "{$label}", "label": "Device Label" }, { "slug": "{$message}", "label": "Syslog Message" } ],
"restorable": true
},
{ "name": "legacy_device_down", "label": "Legacy: Device Down", "variables": [ "…" ], "restorable": true }
]
Get one email template
GET/api/email-templates/{id}alerts
Returns a single template by id.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Response 200 — the bare template object.
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Template not found"} | No template with that id. |
Update an email template
PUT/api/email-templates/{id}alerts
Edits a template’s label, description, bodies, or class. All fields optional.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | no | Max 255. |
description | string | no | Max 1024; nullable. |
email_template | string | no | HTML body; nullable. |
alt_template | string | no | Plain-text alternate; nullable. |
class | string | no | One of the modern classes or legacy_device_down; null/empty clears it. |
Response 200 — the updated template object.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
404 | {"error":"Template not found"} | No template with that id. |
Restore an email template
POST/api/email-templates/{id}/restorealerts
Resets a template’s body and alternate body to the canonical seed for its class. Only works on rows whose class matches a known canonical entry; custom rows (class null or unrecognized) cannot be restored. Takes no body.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Response 200 — the restored template object.
Errors
| Status | Body | When |
|---|---|---|
422 | {"error":"No canonical default exists for this template …"} | The row’s class has no seeded default. |
404 | {"error":"Template not found"} | No template with that id. |
Example
curl -sS -X POST https://APPLIANCE/api/email-templates/3/restore \
-H "Authorization: Bearer $TOKEN"
Maintenance windows
Maintenance windows are reusable suppression schedules. They are a shared catalog; routing rules and legacy alert triggers attach to them. This section covers the catalog CRUD. The Router configuration section covers attaching windows to a specific legacy alert.
List maintenance windows
GET/api/alerts/maintenance-windowsalerts
Returns the maintenance-window catalog. Note the response uses short, remapped field names (hour, day, date, month, unit, duration) — different from the names the write endpoint expects.
Response 200
[
{
"id": 1,
"label": "Nightly backup",
"hour": 2,
"day": null,
"date": null,
"month": null,
"unit": "day",
"duration": 60
}
]
Create or update a maintenance window
POST/api/alerts/maintenance-windowsalerts
Creates a window, or updates one when id is supplied. The required schedule fields depend on recurrence_unit: day needs schedule_hour; week needs schedule_week + schedule_hour; month needs schedule_day + schedule_hour; dawom (day-of-week-of-month) needs schedule_week + schedule_month + schedule_hour.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | no | Present = update; absent = create. |
label | string | yes | — |
recurrence_unit | string | yes | One of day, week, month, dawom. |
maintenance_duration | integer | yes | Duration in minutes. |
schedule_hour | integer | conditional | Required for all units. |
schedule_week | integer | conditional | Required for week and dawom. |
schedule_day | integer | conditional | Required for month. |
schedule_month | integer | conditional | Required for dawom. |
Response 201 (create) / 200 (update)
{ "id": 7 }
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
Example
curl -sS -X POST https://APPLIANCE/api/alerts/maintenance-windows \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"label":"Nightly backup","recurrence_unit":"day","schedule_hour":2,"maintenance_duration":60}'
Delete a maintenance window
POST/api/alerts/maintenance-windows/deletealerts
Deletes a window from the catalog. The id is passed in the body, not the URL.
This removes the maintenance window from the catalog and detaches it from any rules or triggers that reference it.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | yes | Window id to delete. |
Response 200
{ "success": "Maintenance window deleted" }
Router configuration
These endpoints operate on a specific alert target, identified by a {source} (modern or legacy) and an {id}. Router config (severity and throttle settings) works for both sources. The maintenance-window attach/detach/sync endpoints are legacy-only — the route rejects modern — because modern alerts carry suppression on their routing rules, not on the alert itself.
For legacy targets, {id} is the trigger id; for modern targets, {id} is the alert id. Tag-scoping applies to legacy targets (a restricted user only reaches targets on visible devices); modern targets are class-wide.
Get target router config
GET/api/alerts/{source}/{id}/router-configalerts
Returns the severity and throttle settings for the target, with defaults filled in for any unset field.
Path parameters
| Name | Type | Notes |
|---|---|---|
source | string | modern or legacy. |
id | integer | Alert id (modern) or trigger id (legacy). |
Response 200
{
"renotify_minutes": 0,
"throttle_flip_count": 0,
"throttle_flip_window_minutes": 5,
"throttle_stable_minutes": 5,
"severity": 3
}
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Alert not found"} | No matching target (or outside your tag scope). |
Update target router config
POST/api/alerts/{source}/{id}/router-configalerts
Updates any subset of severity and throttle fields on the target. Only the fields present in the body are written.
Path parameters
| Name | Type | Notes |
|---|---|---|
source | string | modern or legacy. |
id | integer | Alert id (modern) or trigger id (legacy). |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
severity | integer | no | 1–5. |
renotify_minutes | integer | no | ≥ 0. |
throttle_flip_count | integer | no | ≥ 0. |
throttle_flip_window_minutes | integer | no | ≥ 0. |
throttle_stable_minutes | integer | no | ≥ 0. |
Response 200
{ "message": "Router config updated" }
Errors
| Status | Body | When |
|---|---|---|
422 | {"errors":{...}} | Validation failed. |
404 | {"error":"Alert not found"} | No matching target. |
Get target maintenance windows
GET/api/alerts/{source}/{id}/maintenance-windowsalerts
Returns the maintenance windows attached to a legacy target. The route accepts only source=legacy.
Path parameters
| Name | Type | Notes |
|---|---|---|
source | string | Must be legacy. |
id | integer | Trigger id. |
Response 200 — an array of windows in the full-field shape (schedule_hour, schedule_week, schedule_day, schedule_month, recurrence_unit, maintenance_duration).
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Alert not found"} | No matching legacy target. |
Attach target maintenance windows
POST/api/alerts/{source}/{id}/maintenance-windows/attachalerts
Attaches one or more catalog windows to a legacy target without disturbing existing attachments. Legacy-only.
Path parameters
| Name | Type | Notes |
|---|---|---|
source | string | Must be legacy. |
id | integer | Trigger id. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
maintenance_window_ids | integer[] | yes | Each must exist in the catalog. |
Response 200
{ "message": "Maintenance windows attached successfully" }
Errors
| Status | Body | When |
|---|---|---|
422 | {"errors":{...}} | Validation failed. |
404 | {"error":"Alert not found"} | No matching legacy target. |
Detach target maintenance windows
POST/api/alerts/{source}/{id}/maintenance-windows/detachalerts
Removes the given windows from a legacy target’s attachments. Same body and responses as attach (message reads “detached successfully”). Legacy-only.
Sync target maintenance windows
POST/api/alerts/{source}/{id}/maintenance-windows/syncalerts
Replaces a legacy target’s attachments with exactly the supplied set — anything not listed is detached. Same body and responses as attach (message reads “synced successfully”). Legacy-only.
Example
curl -sS -X POST https://APPLIANCE/api/alerts/legacy/4012/maintenance-windows/sync \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"maintenance_window_ids":[1,7]}'
Outlets
Outlets are delivery destinations. Each has a type (email, webhook, slack, teams, discord), a config object holding the type-specific settings, an optional rate limit, and an optional template binding. Routing rules reference outlets by id to decide where events go.
Some outlet config fields are secrets — currently the authorization_header on a webhook outlet. On read, a set secret is masked as {"__encrypted__": true} rather than returned in clear. On write, send the sentinel back unchanged (or omit/blank the field) to preserve the stored secret; send a new plaintext value to replace it. Config keys that aren’t valid for the outlet type are silently dropped.
The o365 outlet type may appear on pre-existing rows and is rendered by the UI, but it is not accepted by the create or update validator — you cannot create or convert an outlet to o365. Only email, webhook, slack, teams, and discord are writable.
System outlets (internal audit targets, config.system = true) are excluded from the list and cannot be edited or deleted through the API.
List outlets
GET/api/outletsalerts
Returns all non-system outlets, ordered by id, with secrets masked. Each outlet includes a trimmed template relation (id, name, outlet_type, label).
Response 200
[
{
"id": 5,
"name": "Ops email",
"type": "email",
"config": { "emails": ["ops@example.com"] },
"enabled": true,
"rate_limit_per_min": null,
"template_id": null,
"email_template_id": 3,
"template": null
},
{
"id": 6,
"name": "Slack #alerts",
"type": "slack",
"config": { "url": "https://hooks.slack.com/services/…" },
"enabled": true,
"template_id": 11,
"template": { "id": 11, "name": "slack_default", "outlet_type": "slack", "label": "Slack default" }
}
]
Get one outlet
GET/api/outlets/{id}alerts
Returns a single outlet with its full template relation and secrets masked. Reachable even for system outlets (so a UI can explain an existing reference).
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Outlet id. |
Response 200 — the outlet object.
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Outlet not found"} | No outlet with that id. |
Create an outlet
POST/api/outletsalerts
Creates a delivery outlet. The config shape depends on type — see the table below. Secret fields are encrypted at rest; the response returns them masked.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Max 255; must be unique. |
type | string | yes | email, webhook, slack, teams, or discord (not o365). |
config | object | no | Type-specific (below). Unknown keys dropped. |
enabled | boolean | no | — |
rate_limit_per_min | integer | no | ≥ 1; nullable. |
template_id | integer | no | Outlet (payload) template id; for webhook-style outlets. |
email_template_id | integer | no | Email template id; for email outlets. |
Config keys by type: email → email, emails; webhook → url, authorization_header (secret), headers; slack / teams / discord → url, headers.
Response 201 — the created outlet (secrets masked).
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
409 | {"error":"An outlet with that name already exists."} | Duplicate name. |
Example
curl -sS -X POST https://APPLIANCE/api/outlets \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Slack #alerts",
"type": "slack",
"config": { "url": "https://hooks.slack.com/services/T000/B000/XXX" },
"template_id": 11,
"enabled": true
}'
Update an outlet
PUT/api/outlets/{id}alerts
Updates an outlet. All fields optional. To preserve a stored secret, omit it or replay the {"__encrypted__": true} sentinel; to change it, send the new plaintext. System outlets are rejected with 403.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Outlet id. |
Request body — same fields as create, all optional.
Response 200 — the updated outlet (secrets masked).
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
403 | {"error":"Cannot modify system outlet (reserved for audit)."} | Target is a system outlet. |
404 | {"error":"Outlet not found"} | No outlet with that id. |
409 | {"error":"An outlet with that name already exists."} | Duplicate name. |
Delete an outlet
DELETE/api/outlets/{id}alerts
Deletes an outlet.
This permanently removes the outlet. Routing rules referencing it lose that destination. System outlets cannot be deleted.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Outlet id. |
Response 200
{ "deleted": true }
Errors
| Status | Body | When |
|---|---|---|
403 | {"error":"Cannot delete system outlet (reserved for audit)."} | Target is a system outlet. |
404 | {"error":"Outlet not found"} | No outlet with that id. |
Send a test message
POST/api/outlets/testalerts
Sends a live test message using the submitted form state — not a saved outlet — so you can verify a brand-new config or an unsaved edit before committing it. The message is enqueued for delivery and the call returns immediately with the queue row id; poll Get test status to watch it settle. If outlet_id is supplied, any {"__encrypted__": true} sentinel in config is resolved against that saved outlet’s stored secret.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | yes | email, webhook, slack, teams, or discord. |
config | object | yes | The config to test. |
template_id | integer | no | Outlet template to render (webhook-style); defaults to the type’s <type>_default. |
outlet_id | integer | no | Existing outlet to resolve masked secrets against. |
Response 200
{ "kind": "webhook", "id": 8841 }
The kind is email or webhook (Slack/Teams/Discord all queue as webhook); id is the outbox row id to poll.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
422 | {"error":"email outlet has no recipients configured"} | No recipients on an email test. |
422 | {"error":"HTTP outlet is missing url"} | No URL on a webhook-style test. |
422 | {"error":"no payload template resolvable for outlet type …"} | No template found. |
Example
curl -sS -X POST https://APPLIANCE/api/outlets/test \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"type":"email","config":{"emails":["ops@example.com"]}}'
Get test status
GET/api/outlets/test/{kind}/{id}alerts
Polls the delivery state of a test message queued by Send a test message. Poll until terminal is true.
Path parameters
| Name | Type | Notes |
|---|---|---|
kind | string | email or webhook (from the test response). |
id | integer | The outbox row id from the test response. |
Response 200
{
"kind": "webhook",
"id": 8841,
"dispatched_at": "2026-06-15T11:20:05Z",
"attempted_at": "2026-06-15T11:20:05Z",
"result_code": 200,
"server_response": "ok",
"retry_count": 0,
"max_retries": 1,
"terminal": true
}
Errors
| Status | Body | When |
|---|---|---|
400 | {"error":"invalid kind"} | kind is not email/webhook. |
404 | {"error":"test row not found"} | No such row (for email tests, a successful send deletes the row, which reads as gone). |
Routing rules
Routing rules connect alert events to outlets. A rule matches on alert class, severity, device tags, and event-key patterns, scoped to global, a specific device, a legacy handler, or a single modern alert. Every matching rule contributes its outlets and the appliance unions them — rules do not shadow each other.
This is one of the tag-scoped surfaces. Tag-restricted users see only rules whose scope or tag_filters overlap their tags; global and alert scoped rules are hidden from them. Writes are re-checked against the same scope, so a restricted user cannot author a rule they couldn’t see.
List routing rules
GET/api/routing-rulesalerts
Returns routing rules, ordered to mirror the engine’s evaluation order (legacy → alert → device → tag → global, then id). Tag-scoped.
Response 200
[
{
"id": 21,
"name": "Page on critical syslog",
"scope": "global",
"scope_id": null,
"alert_classes": ["syslog_log"],
"severities": ["1","2"],
"tag_filters": ["router"],
"key_patterns": [],
"outlet_ids": [5, 6],
"enabled": true,
"notify_on_resolved": true
}
]
Get one routing rule
GET/api/routing-rules/{id}alerts
Returns a single rule (tag-scoped).
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Rule id. |
Response 200 — the bare rule object.
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Routing rule not found"} | No rule with that id (or outside your tag scope). |
Create a routing rule
POST/api/routing-rulesalerts
Creates a routing rule. scope_id shape depends on scope: it must be absent for global; a device id for device; an alert_handlers id for legacy; an alerts id for alert. (The legacy tag scope is no longer creatable — express “fire on devices tagged X” as scope=global plus tag_filters=["x"].)
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Max 255. |
scope | string | yes | global, device, legacy, or alert. |
scope_id | string | conditional | Null for global; integer id for the others (existence-checked per scope). |
alert_classes | string[] | no | Subset of legacy, syslog_log, event_log, eve_log. |
severities | string[] | no | Subset of "1"–"5". |
tag_filters | string[] | no | Tag slugs; AND-semantics. |
key_patterns | string[] | no | Event-key match patterns. |
outlet_ids | integer[] | no | Outlet ids to dispatch to. |
enabled | boolean | no | — |
notify_on_resolved | boolean | no | Whether to send a RESOLVED notification. |
maintenance_window_ids | integer[] | no | Catalog windows to attach. |
Response 201 — the created rule (with its attached windows).
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
403 | {"errors":{"scope":["…"]}} | A tag-restricted user tried to write outside their scope. |
Example
curl -sS -X POST https://APPLIANCE/api/routing-rules \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Page on critical syslog",
"scope": "global",
"alert_classes": ["syslog_log"],
"severities": ["1","2"],
"tag_filters": ["router"],
"outlet_ids": [5, 6],
"enabled": true,
"notify_on_resolved": true
}'
Update a routing rule
PUT/api/routing-rules/{id}alerts
Updates a rule. All fields optional; same scope rules and scope re-check as create.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Rule id. |
Request body — same fields as create, all optional.
Response 200 — the updated rule.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed. |
403 | {"errors":{"scope":["…"]}} | Out-of-scope write. |
404 | {"error":"Routing rule not found"} | No rule with that id. |
Delete a routing rule
DELETE/api/routing-rules/{id}alerts
Deletes a routing rule.
Removing a rule stops the delivery it drove. Events still fire and are recorded; they simply stop reaching that rule’s outlets.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Rule id. |
Response 200
{ "deleted": true }
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Routing rule not found"} | No rule with that id. |
Outlet templates
Outlet templates are JSON payload templates for webhook-style outlets. They use {$var} (string substitution) and {@var} (raw/JSON substitution) placeholders, rendered at delivery time. Each template is bound to an outlet type and has a unique name.
List outlet templates
GET/api/outlet-templatesalerts
Returns outlet templates, ordered by type then name. Filter to one type with the outlet_type query parameter.
Query parameters
| Name | Type | Notes |
|---|---|---|
outlet_type | string | Optional. Filter to one outlet type. |
Response 200
[
{
"id": 11,
"name": "slack_default",
"outlet_type": "slack",
"label": "Slack default",
"description": "Canonical Slack payload.",
"payload_template": "{\"text\": \"{$subject}: {$message}\"}"
}
]
Get one outlet template
GET/api/outlet-templates/{id}alerts
Returns a single template.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Response 200 — the bare template object.
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Outlet template not found"} | No template with that id. |
Create an outlet template
POST/api/outlet-templatesalerts
Creates a payload template. The payload_template is checked: after placeholder substitution it must parse as valid JSON, or the call is rejected.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Max 255; must be unique. |
outlet_type | string | yes | webhook, slack, teams, discord, or o365. (Templates accept o365 even though outlets cannot be created as o365 — so an existing o365 outlet on a legacy install still has a template to render.) |
label | string | no | Max 255; nullable. |
description | string | no | Nullable. |
payload_template | string | yes | Must be valid JSON after placeholder substitution. |
Response 201 — the created template.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed, or {"errors":{"payload_template":["must be valid JSON after placeholder substitution"]}}. |
Example
curl -sS -X POST https://APPLIANCE/api/outlet-templates \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "teams_compact",
"outlet_type": "teams",
"payload_template": "{\"text\": \"{$subject}\"}"
}'
Update an outlet template
PUT/api/outlet-templates/{id}alerts
Updates a template. All fields optional; if payload_template is supplied it is re-validated.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Request body — same fields as create, all optional.
Response 200 — the updated template.
Errors
| Status | Body | When |
|---|---|---|
400 | {"errors":{...}} | Validation failed (including the JSON-validity check). |
404 | {"error":"Outlet template not found"} | No template with that id. |
Delete an outlet template
DELETE/api/outlet-templates/{id}alerts
Deletes a payload template.
Outlets bound to this template are not deleted — they fall back to the seeded default for their type at delivery time.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Template id. |
Response 200
{ "deleted": true }
Errors
| Status | Body | When |
|---|---|---|
404 | {"error":"Outlet template not found"} | No template with that id. |
Legacy handler catalog
A read-only catalog of legacy alert handlers, used to populate the scope-legacy dropdown when building a routing rule. The full legacy alert surface (definitions and history) is documented under Legacy alerts below.
List legacy handlers
GET/api/legacy-handlersalerts
Returns legacy handlers with enough detail to render a descriptive label per option. Tag-scoped: a restricted user only sees handlers on devices they can view.
Response 200
[
{
"id": 4012,
"trigger_id": 8801,
"name": "core-sw1 — port 443 down",
"type": "port",
"tracker_name": "443/tcp",
"pattern": null,
"threshold": 1,
"comp_exp": "==",
"active": true,
"device_id": 17,
"device_name": "core-sw1"
}
]
Example
curl -sS https://APPLIANCE/api/legacy-handlers \
-H "Authorization: Bearer $TOKEN"
Legacy alerts
Before the class-based alert engine, the appliance used per-device legacy alerts: a handler paired with a trigger that watched one specific tracker on one device — a disk, an interface, a TCP port, a custom OID, a latency check, a monitored URL, or a local network. These legacy alerts are still supported and still fire. This section documents their read/write endpoints and the shared alert history ledger that records every alert that fired, legacy or modern.
For new monitoring, prefer the modern alert engine described in the sections above: it is class-based, routed through outlets, and it is what the alert wizard builds. Two legacy alert types are now closed to new use — creating a legacy syslog alert is rejected outright, and creating a legacy Windows event log alert is rejected in favor of the modern Event Log class (existing legacy event-log alerts remain editable).
These endpoints belong to the alerts functional area; an MCP or OAuth token needs the mcp:alerts scope, and the calling user must hold the alerts permission. Legacy alerts are tag-scoped: a tag-restricted user can only read, write, or delete handlers on devices within their tag set, and alert-history rows are filtered to visible devices. A user with no tag restriction sees everything; sa bypasses.
These endpoints predate the modern conventions and return a custom envelope. Success is typically {"status": 201, "hasError": false, ...} (note: 201 is sent as a field in a 200 HTTP response, not as the HTTP status). Failure is {"status": false, "hasError": true, "message": "..."} — also returned with HTTP 200. Do not key automation off the HTTP status alone; read the hasError field. A handful of paths do return real HTTP 403/404; those are noted per endpoint.
Legacy alert definitions
Legacy alerts are addressed two ways. A handler is the saved alert (it has an id); a trigger is its watch condition (it has a trigger_id). The read endpoints return rows from a combined view that carries both, plus device and tracker context. Writes take a type (the tracker kind) and a tracker (the id of the row being watched) and create or update the handler/trigger pair.
List legacy alerts
POST/api/getAlertsalerts
Returns every legacy alert. Despite the verb, this is a read with no required body. Results are tag-scoped to the caller’s visible devices. The pattern field is returned decoded from JSON where it holds structured data.
Response 200
{
"status": 201,
"hasError": false,
"alerts": [
{
"id": 4012,
"trigger_id": 8801,
"source": "legacy",
"name": "core-sw1 — port 443 down",
"type": "port",
"device_id": 17,
"device_name": "core-sw1",
"threshold": 1,
"comp_exp": "==",
"pattern": null,
"active": true
}
]
}
A tag-restricted user with no visible devices gets {"status":201,"hasError":false,"alerts":[]}.
Example
curl -sS -X POST https://APPLIANCE/api/getAlerts \
-H "Authorization: Bearer $TOKEN"
Create or update one legacy alert
POST/api/setAlertalerts
Creates a legacy alert, or updates an existing one when trigger_id/id are supplied. The type selects which tracker table the alert watches and shapes the stored comparison; tracker is the id of the watched row in that table.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
tracker | integer | yes | Id of the watched row (e.g. an interface, disk, port, or OID id). |
type | string | yes | Tracker kind — see the supported set below. |
name | string | no | Handler label. |
threshold | number | no | Trigger threshold; defaults to -1. |
comp_exp | string | no | Comparison operator; often forced by type. |
pattern | mixed | no | Type-specific match pattern (object for event-log/interface STATUS, etc.). |
active | boolean | no | Defaults to true. |
recipients | mixed | no | Stored on the handler (legacy field; delivery is rule-driven now). |
trigger_id | integer | no | Present = update the existing trigger. |
id | integer | no | Present = update the existing handler. |
Supported type values: disk, oid, interface, latency, port, url, localnet, and eventlog (edit-only). The syslog type is rejected. The eventlog type is rejected for new alerts — only edits to an already-existing event-log trigger are allowed.
Response 200 — on success, the saved alert echoed back:
{ "status": 201, "hasError": false, "message": "Success", "alert": { "id": 4012, "type": "port", "…": "…" } }
On failure (including the closed syslog/eventlog paths and tag-scope violations), HTTP 200 with:
{ "status": false, "hasError": true, "message": "Legacy syslog alerts are deprecated. Migrate to the modern alert system." }
Example
curl -sS -X POST https://APPLIANCE/api/setAlert \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"type":"interface","tracker":5521,"name":"WAN down","pattern":"STATUS","active":true}'
Create or update many legacy alerts
POST/api/setAlertsalerts
Batch form of setAlert. Applies one type/tracker across an array of alert specs, each of which may create, update, or (with delete: true and a trigger_id) remove a trigger. The same type restrictions apply (syslog rejected; eventlog edit-only).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
tracker | integer | yes | Watched row id, shared by all entries. |
type | string | yes | Tracker kind (same set as setAlert). |
alerts | array | yes | Per-alert specs. |
Each entry in alerts accepts the per-alert fields from setAlert (name, threshold, comp_exp, pattern, active, recipients, notes, trigger_id, id) plus an optional delete boolean (with trigger_id) to remove that trigger.
Response 200
{ "status": 201, "hasError": false, "message": "Success" }
On failure: {"status": false, "hasError": true, "message": "..."}.
Example
curl -sS -X POST https://APPLIANCE/api/setAlerts \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"type": "disk",
"tracker": 990,
"alerts": [
{ "name": "Root FS > 90%", "threshold": 90, "active": true },
{ "trigger_id": 8042, "delete": true }
]
}'
Delete a legacy alert
POST/api/deleteAlertalerts
Deletes a legacy alert by handler id. This also removes its trigger and any maintenance windows attached to that trigger. Tag-scope is enforced on the resolved device.
This permanently deletes the handler, its trigger, and that trigger’s maintenance-window attachments.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | yes | Handler id. |
Response 200
{ "status": 201, "hasError": false, "message": "Success" }
On failure (missing id, handler not found, or out of tag scope): HTTP 200 with {"status": false, "hasError": true, "message": "..."}.
Example
curl -sS -X POST https://APPLIANCE/api/deleteAlert \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"id":4012}'
Alert history
The alert-history ledger records one row per incident — every alert that fired, legacy or modern — with its lifecycle timestamps and a roll-up of which delivery methods were used. A separate per-dispatch log drills into the individual deliveries for one incident. History rows are tag-scoped to visible devices.
Get global alert history
GET/api/getAlertHistoryalerts
Returns up to 500 of the most recent incidents across all visible devices, newest first. Each row rolls up the delivery outlets used (outlet_types) and dispatch counts.
Response 200
{
"status": 201,
"message": [
{
"id": 9001,
"source": "modern",
"alert_id": 12,
"trigger_id": null,
"event_key": "…",
"alert_label": "SSH auth failures",
"alert_type": "syslog_log",
"severity": 2,
"subject": "SSH auth failures on web1",
"opened_at": "2026-06-15T10:00:00Z",
"last_event_at": "2026-06-15T10:00:00Z",
"resolved_at": "2026-06-15T10:00:00Z",
"device_id": 22,
"device_name": "web1",
"device_ip": "10.0.0.22",
"device_profile": "linux-server",
"outlet_types": ["email", "slack"],
"dispatch_count": 2,
"failed_count": 0
}
]
}
A tag-restricted user with no visible devices gets {"status":201,"message":[]}. On error: {"status": false, "hasError": true, "message": [], "error": "..."}.
Modern log-stream incidents are opened already closed — a log entry is a discrete occurrence, not a state — so resolved_at equals opened_at and each new match lands its own row. Only legacy device-status incidents stay open (resolved_at null) until the condition clears.
Example
curl -sS https://APPLIANCE/api/getAlertHistory \
-H "Authorization: Bearer $TOKEN"
Get device alert history
GET/api/device/{device}/alert-historyalerts
Returns up to 500 incidents for a single device, newest first — the same row shape as the global history. Used by the per-device Alerts pane.
Path parameters
| Name | Type | Notes |
|---|---|---|
device | integer | Device id. |
Response 200 — {"status": 201, "message": [ … ]} with the incident rows.
Errors
| Status | Body | When |
|---|---|---|
403 | {"status":403,"message":"Forbidden"} | The device is outside your tag scope (real HTTP 403). |
Example
curl -sS https://APPLIANCE/api/device/22/alert-history \
-H "Authorization: Bearer $TOKEN"
Get incident dispatch log
GET/api/alert-history/{id}/logalerts
Returns the per-dispatch ledger for one incident — every delivery attempt to every outlet, with recipients, status, attempt counts, and any error. Tag-scope is re-checked against the incident’s device.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | integer | Incident (alert-history) id. |
Response 200
{
"status": 201,
"message": [
{
"id": 55001,
"history_id": 9001,
"outlet_id": 5,
"outlet_type": "email",
"notification_type": "TRIGGERED",
"recipients": "ops@example.com",
"subject": "SSH auth failures on web1",
"status": "sent",
"attempt_count": 1,
"last_attempt": "2026-06-15T10:00:05Z",
"error": null,
"payload": "…",
"created_at": "2026-06-15T10:00:05Z",
"delivered_at": "2026-06-15T10:00:06Z"
}
]
}
Errors
| Status | Body | When |
|---|---|---|
404 | {"status":404,"message":"Incident not found"} | No incident with that id (real HTTP 404). |
403 | {"status":403,"message":"Forbidden"} | Incident’s device is outside your tag scope (real HTTP 403). |
Example
curl -sS https://APPLIANCE/api/alert-history/9001/log \
-H "Authorization: Bearer $TOKEN"