Netmon Docs · API Reference

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.

Inconsistent response envelopes

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

NameTypeNotes
idintegerAlert id.

Response 200 — the bare alert object (same shape as a list element).

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
labelstringyesMax 255.
classstringyesOne of syslog_log, event_log, eve_log.
severityintegeryes1–5.
parametersobjectconditionalClass-specific match parameters; validated per class. Most classes require occurrences_min and lookback_minutes.
enabledbooleannoDefaults to the column default.
scopestringconditionalglobal (the default), device, or tag. Required whenever scope_id is sent.
scope_idstringconditionalOmit or null for global; a devices.id for device (existence-checked); a tag slug for tag. An empty string is stored as null.
tag_filtersstring[]noTag slugs, AND-semantics — the alert fires only on devices carrying every listed slug. Defaults to [] (no narrowing).
renotify_minutesintegernoMinutes between reminder notifications; 0/null disables.
throttle_flip_countintegernoFlip-suppression count.
throttle_flip_window_minutesintegernoFlip-suppression window.
throttle_stable_minutesintegernoStable-period minutes.
Alert scope decides firing, routing rules decide delivery

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.

Throttle fields are stored but inert

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

StatusBodyWhen
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

NameTypeNotes
idintegerAlert id.

Request body — same fields as create, all optional. A class in the body is ignored.

Response 200 — the updated alert object.

Errors

StatusBodyWhen
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.

Destructive

This removes the alert definition. The alert stops evaluating immediately.

Path parameters

NameTypeNotes
idintegerAlert id.

Response 200

{ "deleted": true }

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerAlert id.

Response 200 — the alert object with its new enabled value.

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerTemplate id.

Response 200 — the bare template object.

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerTemplate id.

Request body

FieldTypeRequiredNotes
labelstringnoMax 255.
descriptionstringnoMax 1024; nullable.
email_templatestringnoHTML body; nullable.
alt_templatestringnoPlain-text alternate; nullable.
classstringnoOne of the modern classes or legacy_device_down; null/empty clears it.

Response 200 — the updated template object.

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerTemplate id.

Response 200 — the restored template object.

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
idintegernoPresent = update; absent = create.
labelstringyes
recurrence_unitstringyesOne of day, week, month, dawom.
maintenance_durationintegeryesDuration in minutes.
schedule_hourintegerconditionalRequired for all units.
schedule_weekintegerconditionalRequired for week and dawom.
schedule_dayintegerconditionalRequired for month.
schedule_monthintegerconditionalRequired for dawom.

Response 201 (create) / 200 (update)

{ "id": 7 }

Errors

StatusBodyWhen
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.

Destructive

This removes the maintenance window from the catalog and detaches it from any rules or triggers that reference it.

Request body

FieldTypeRequiredNotes
idintegeryesWindow 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

NameTypeNotes
sourcestringmodern or legacy.
idintegerAlert 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

StatusBodyWhen
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

NameTypeNotes
sourcestringmodern or legacy.
idintegerAlert id (modern) or trigger id (legacy).

Request body

FieldTypeRequiredNotes
severityintegerno1–5.
renotify_minutesintegerno≥ 0.
throttle_flip_countintegerno≥ 0.
throttle_flip_window_minutesintegerno≥ 0.
throttle_stable_minutesintegerno≥ 0.

Response 200

{ "message": "Router config updated" }

Errors

StatusBodyWhen
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

NameTypeNotes
sourcestringMust be legacy.
idintegerTrigger 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

StatusBodyWhen
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

NameTypeNotes
sourcestringMust be legacy.
idintegerTrigger id.

Request body

FieldTypeRequiredNotes
maintenance_window_idsinteger[]yesEach must exist in the catalog.

Response 200

{ "message": "Maintenance windows attached successfully" }

Errors

StatusBodyWhen
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.

Encrypted field

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.

o365 is read-only

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

NameTypeNotes
idintegerOutlet id.

Response 200 — the outlet object.

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
namestringyesMax 255; must be unique.
typestringyesemail, webhook, slack, teams, or discord (not o365).
configobjectnoType-specific (below). Unknown keys dropped.
enabledbooleanno
rate_limit_per_minintegerno≥ 1; nullable.
template_idintegernoOutlet (payload) template id; for webhook-style outlets.
email_template_idintegernoEmail template id; for email outlets.

Config keys by type: emailemail, emails; webhookurl, authorization_header (secret), headers; slack / teams / discordurl, headers.

Response 201 — the created outlet (secrets masked).

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerOutlet id.

Request body — same fields as create, all optional.

Response 200 — the updated outlet (secrets masked).

Errors

StatusBodyWhen
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.

Destructive

This permanently removes the outlet. Routing rules referencing it lose that destination. System outlets cannot be deleted.

Path parameters

NameTypeNotes
idintegerOutlet id.

Response 200

{ "deleted": true }

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
typestringyesemail, webhook, slack, teams, or discord.
configobjectyesThe config to test.
template_idintegernoOutlet template to render (webhook-style); defaults to the type’s <type>_default.
outlet_idintegernoExisting 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

StatusBodyWhen
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

NameTypeNotes
kindstringemail or webhook (from the test response).
idintegerThe 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

StatusBodyWhen
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

NameTypeNotes
idintegerRule id.

Response 200 — the bare rule object.

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
namestringyesMax 255.
scopestringyesglobal, device, legacy, or alert.
scope_idstringconditionalNull for global; integer id for the others (existence-checked per scope).
alert_classesstring[]noSubset of legacy, syslog_log, event_log, eve_log.
severitiesstring[]noSubset of "1""5".
tag_filtersstring[]noTag slugs; AND-semantics.
key_patternsstring[]noEvent-key match patterns.
outlet_idsinteger[]noOutlet ids to dispatch to.
enabledbooleanno
notify_on_resolvedbooleannoWhether to send a RESOLVED notification.
maintenance_window_idsinteger[]noCatalog windows to attach.

Response 201 — the created rule (with its attached windows).

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerRule id.

Request body — same fields as create, all optional.

Response 200 — the updated rule.

Errors

StatusBodyWhen
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.

Destructive

Removing a rule stops the delivery it drove. Events still fire and are recorded; they simply stop reaching that rule’s outlets.

Path parameters

NameTypeNotes
idintegerRule id.

Response 200

{ "deleted": true }

Errors

StatusBodyWhen
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

NameTypeNotes
outlet_typestringOptional. 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

NameTypeNotes
idintegerTemplate id.

Response 200 — the bare template object.

Errors

StatusBodyWhen
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

FieldTypeRequiredNotes
namestringyesMax 255; must be unique.
outlet_typestringyeswebhook, 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.)
labelstringnoMax 255; nullable.
descriptionstringnoNullable.
payload_templatestringyesMust be valid JSON after placeholder substitution.

Response 201 — the created template.

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerTemplate id.

Request body — same fields as create, all optional.

Response 200 — the updated template.

Errors

StatusBodyWhen
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.

Destructive

Outlets bound to this template are not deleted — they fall back to the seeded default for their type at delivery time.

Path parameters

NameTypeNotes
idintegerTemplate id.

Response 200

{ "deleted": true }

Errors

StatusBodyWhen
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.

Response envelope

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

FieldTypeRequiredNotes
trackerintegeryesId of the watched row (e.g. an interface, disk, port, or OID id).
typestringyesTracker kind — see the supported set below.
namestringnoHandler label.
thresholdnumbernoTrigger threshold; defaults to -1.
comp_expstringnoComparison operator; often forced by type.
patternmixednoType-specific match pattern (object for event-log/interface STATUS, etc.).
activebooleannoDefaults to true.
recipientsmixednoStored on the handler (legacy field; delivery is rule-driven now).
trigger_idintegernoPresent = update the existing trigger.
idintegernoPresent = 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

FieldTypeRequiredNotes
trackerintegeryesWatched row id, shared by all entries.
typestringyesTracker kind (same set as setAlert).
alertsarrayyesPer-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.

Destructive

This permanently deletes the handler, its trigger, and that trigger’s maintenance-window attachments.

Request body

FieldTypeRequiredNotes
idintegeryesHandler 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

NameTypeNotes
deviceintegerDevice id.

Response 200{"status": 201, "message": [ … ]} with the incident rows.

Errors

StatusBodyWhen
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

NameTypeNotes
idintegerIncident (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

StatusBodyWhen
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"