If-None-Match/If-Match ETag contract, the same 428/412 rules — only the collection name and body differ.
Reading alerts
Reads are always safe:GET /v1/metric-alerts— summaries, with a cache-only collection ETag forIf-None-Match.GET /v1/metric-alerts/{slug}— one alert as a bare object, plus its resource ETag (an HTTP header) for conditional writes.
Writing alerts
The document
The exact contract isMetricAlertInput in the OpenAPI spec; the essentials:
display_name— required, 1–255 characters. Theslugis the immutable URL identity, never in the body.severity— required; one ofinfo,warning,critical.enabled— required boolean, honored atomically:truestarts evaluating immediately, and omitting the key is a400, not a silent disable.condition.expression— required PromQL: a top-level comparison of a vector query against one finite number literal (>,>=,<,<=), e.g.sum(rate(http_requests_total{"http.response.status_code"=~"5.."}[5m])) > 5. Every selector needs an exact metric name.condition.recovery_expression— optional hysteresis: a second comparison pointing the opposite way whose band must not overlap the firing band (firing> 80, recovery< 70). Omitted → recovery is the negation of the firing expression.notifications— required array of integration channel slugs (discover them via/v1/integrations). It’s a set — order doesn’t matter; a duplicate or unknown slug is400;[]is valid (the alert fires but notifies nobody).documentation.markdown— optional runbook (up to 16,384 bytes), delivered verbatim into every notification for this alert. Use inline links:[Runbook](https://…).- Unknown JSON fields are rejected with
400; the body is capped at 1 MiB;Content-Type: application/jsonis required.
Grouped alerts: one incident per series
The condition may return one series or many. A bare aggregate (sum(...)) is one series and fires one incident. A grouped query — sum by ("chain") (rate(m[5m])) > 5 — fires one independent incident per dimension value: chain="ethereum" can fire while chain="bsc" stays healthy, and the notification names the dimension that broke. Grouping is authored only inside the PromQL by (...) clause — there is no separate group-by field.
Sustained-ness: there is no for
Express duration inside the window function, and pick the function to match the intent:
Firing and recovery may use different window functions as long as their numeric bands don’t overlap — e.g. firing
min_over_time(m[5m]) > 600 with recovery max_over_time(m[5m]) < 60 gives a dead-band the signal must fully clear, so the alert can’t flap around a single threshold.
Execution semantics
Evaluation is event-driven, deliberately not Prometheus-compatible: each incoming datapoint for a watched metric re-evaluates the condition. A no-data, non-finite, or query-error evaluation holds the current state and never fires. Each returned series is its own incident keyed on its label set, opened and resolved independently.Create
The conditional-write contract is identical to dashboards: create withIf-None-Match: *, replace/delete with If-Match: "<etag>" from a fresh GET.
Notification channels
GET /v1/integrations and GET /v1/integrations/{slug} list the notification channels an alert can deliver to, as {slug, display_name, type, enabled} — redacted: no webhook URL, headers, or routing keys are ever returned.
- Channels are organization-scoped — the one collection that isn’t per-project. One slug is shared by every project in the org: an alert in project A and one in project B can both notify the channel named
oncall. - This surface is read-only. Create and configure channels in the Mirador web app (see Integrations), then reference them by slug in an alert’s
notifications.
Next Steps
Metrics
Discover the metrics your conditions evaluate
Automations
Trace-based rules and integrations in the web app