Metrics, uptime, alerts and incidents
Module: metrics. See README.md for authentication and ranges.
Endpoints
| Endpoint | Scope | Permission | What it does |
|---|---|---|---|
GET /apps/{app}/metrics/names |
read | metrics.read |
Metric names, kinds and units |
GET /apps/{app}/metrics/series |
read | metrics.read |
One metric aggregated into buckets |
GET /apps/{app}/monitors |
read | metrics.read |
Uptime monitors with 30-day uptime |
POST /apps/{app}/monitors |
write | metrics.write |
Create or update a monitor |
GET /apps/{app}/alerts |
read | metrics.read |
Alert rules |
POST /apps/{app}/alerts |
write | metrics.write |
Create or update an alert rule |
GET /apps/{app}/incidents |
read | metrics.read |
Alert incidents |
Metrics use their own range vocabulary: 1h, 6h, 24h (default), 7d,
30d, 90d, with intervals 1m, 5m, 15m, 1h, 6h, 1d.
Series
?name=<required>&agg=sum|avg|count|p50|p95|p99|max|min&range=24h&interval=&label.<key>=<value>
Label filters are repeated query parameters: ?label.region=eu&label.table=orders.
{
"name": "db.query",
"agg": "p95",
"interval": "15m",
"source": "rollups",
"points": [{ "t": "2026-09-03T10:00:00.000Z", "value": 42.5 }],
"summary": { "count": 9120, "sum": 402000, "min": 1.2, "max": 980, "avg": 44.1 }
}
source says where the answer came from: ranges up to six hours read raw
metric_points, longer ranges read the five-minute rollups. Percentiles over
rollups are computed from a bounded per-bucket sample, so they are close but not
exact — the field is there so you can say which you are quoting.
Monitors
POST /apps/{app}/monitors
{ "name": "Home", "url": "https://example.com/health", "method": "GET",
"expectedStatus": 200, "intervalSeconds": 300, "timeoutMs": 10000, "enabled": true }
Pass id to update. The URL must be a public http(s) address: loopback, private
ranges and link-local addresses are refused with a 400, because a monitor is a
request the platform makes on your behalf. bodyMatch fails the check unless
the response body contains the text; bodyAbsent fails it when the body does.
Both are optional and may be combined.
Rows carry lastStatus, lastCheckedAt, lastLatencyMs, consecutiveFailures
and 30-day uptime.
Alert rules
POST /apps/{app}/alerts
{ "name": "Slow queries", "kind": "metric", "threshold": 500, "comparator": "gt",
"windowMinutes": 5, "cooldownMinutes": 30, "severity": "warning",
"condition": { "name": "db.query", "agg": "p95" }, "channelIds": ["c_1"] }
condition depends on kind:
| Kind | Condition |
|---|---|
metric |
{ name, agg, labels? } — required |
uptime |
{ monitorId } — required |
vital |
{ vital: "LCP" | "CLS" | "INP" | "FCP" | "TTFB", path? } — required |
error_rate |
{ level? } |
event_volume |
{ name? } |
new_issue |
{} |
A rule is bound to the environment resolved from ?env=. Channels must already
exist (create them in the console); an unknown channel id is a 400.
Incidents
?status=open|acknowledged|resolved|all (default open)
An open incident means a rule is firing right now. Acknowledging and resolving are console actions.