PUT on deploy.
Reading dashboards
GET /v1/dashboards
Returns dashboard summaries (without widget bodies) plus a cache-only collection ETag. Re-send it as If-None-Match to receive 304 Not Modified when nothing changed. This collection ETag cannot authorize a write.
GET /v1/dashboards/{slug}
Returns the complete dashboard as a bare object; its opaque revision ETag arrives as an HTTP header — keep it for conditional writes.
GET-by-slug does not support conditional reads — sending its ETag as
If-None-Match still returns 200 with the full body. Only the collection
returns 304.Rendering a stored widget yourself
Runtimeseries widgets through /v1/metrics/query_range; run stat, pie, and table through /v1/metrics/query evaluated at the selected range end (put whole-window aggregation such as sum(increase(m[1h])) inside the PromQL). markdown widgets need no query.
Writing dashboards
The document
The slug is the immutable identity in the URL — 1–63 lowercase letters/digits with internal hyphens — and is never part of the body. The exact body contract isDashboardInput in the OpenAPI spec; the essentials:
title— required, 1–200 characters.description— optional, up to 2,000.default_time_window— required; one of1m, 5m, 15m, 1h, 4h, 12h, 24h, 7d, 30d.widgets— required (may be[]), up to 24. IDs are unique,^[A-Za-z0-9_-]{1,64}$.- Widget
type—timeseries,stat,pie,table, ormarkdown. grid—{x, y, w, h}on 12 columns;x + w <= 12.- Queries are
{ "promql": "..." }— timeseries/table take 1–4, stat/pie exactly 1, markdown none. Every query must evaluate to an instant vector or scalar — a bare range selector is rejected at write time, so wrap it asrate(...)orsum(increase(...)). - Only timeseries supports
style(line, area, bar, stacked_bar) andtime_interval. Leavetime_intervalunset — the bucket then auto-sizes to whatever window the viewer selects. A fixed interval must satisfywindow / time_interval <= 600at every selectable window, or the widget renders as unavailable at wider windows. - A markdown widget provides non-empty
markdown(up to 16,384 UTF-8 bytes). Any widget may overridetime_window. - Grouping and aggregation belong in the PromQL — there is no
group_by,unit, orcolorfield. Unknown JSON fields are rejected with400, not ignored. - The body is capped at 1 MiB and requires
Content-Type: application/json.
The conditional-write contract
Every mutation requires exactly one conditional header:
Supplying both headers is
400; supplying neither is 428. A malformed header value (unquoted ETag, W/"...", If-Match: *, a list of ETags) is 400, not 412 — a client bug to fix, not a race to retry. Replay the ETag verbatim, including quotes; never synthesize one or borrow it from another slug or the collection list. Conditional writes never return 404 — a well-formed If-Match on a vanished slug is 412.
Reapplying an identical body is a 200 no-op with an unchanged ETag.
Create
Replace or delete
GET the latest document and ETag first, apply your changes to the complete document, then PUT/DELETE with If-Match:
412, another writer won: GET again, reapply your intent to the fresh document, and retry — never blindly resend a stale body. On a cleanup 412 whose follow-up GET is 404, the dashboard is already gone — stop; don’t re-create it.
Next Steps
Metrics
The PromQL that widget queries evaluate
Metric alerts
The same slug + ETag contract, for alerting