Overview
A dashboard is one declarative JSON document the platform stores, resolves into cards and lines, and renders.
A dashboard is one JSON document the platform stores and renders. It names a window (the time range the page opens with), a grid layout, and a list of components. The API is the primary authoring surface: the web app lists and draws dashboards, and its editor saves through the same routes.
There are two component types:
chartdraws one card. On its own it must name every record it draws.groupdraws a section of cards: one chart per item of itsfor_each, which iterates every record of an object type or a list of records. The group owns what its cards share: the iterator, the heading, and the chart template. Inside the template, placeholders such as{{ item.name }}fill in per item, and a series without arecorddraws the item's own.
The document is declarative. Nobody enumerates sites; a dashboard over "all sites" keeps following the data as sites are added. Ten charts from a dozen lines:
{
"name": "Power",
"window": {"last": "P2D", "ahead": "P2D"},
"components": [{
"type": "group",
"title": "Sites",
"for_each": {"object": "sites"},
"component": {
"type": "chart",
"title": "{{ item.name }}",
"chart": {
"series": [
{"timeseries": "power", "label": "Actual"},
{"timeseries": "power-forecast", "column": "power", "label": "Forecast"}
]
}
}
}]
}GET /dashboards/{id}/resolve turns the document into concrete cards and lines, which is what the web app and any other renderer draws. POST /dashboards/resolve does the same for an unsaved document: the dry run an author or an agent uses before saving.
Vocabulary
| Word | Meaning |
|---|---|
| document | the JSON a dashboard is written as (POST/PUT body, GET response) |
| component | an entry of components: a chart or a group |
| group | a component that repeats a chart template per item |
| card | one drawn chart, as the resolved form lists it |
| series (document) | one entry of chart.series: a timeseries type on a record, with drawing options |
| line | one drawn series in the resolved form |
| series type | the platform's timeseries slug on a record (power, power-forecast), as in Upload / read timeseries |
Routes and permissions
| Method | Path | Does | Permission |
|---|---|---|---|
| GET | /dashboards | list the workspace's dashboards (summaries, no components) | objects:read |
| POST | /dashboards | create from a document; 201 with the stored document | objects:write |
| GET | /dashboards/schema | the document's JSON Schema | objects:read |
| POST | /dashboards/resolve | resolve an unsaved document, saving nothing | objects:read |
| GET | /dashboards/{id} | the stored document with its envelope | objects:read |
| PUT | /dashboards/{id} | replace the whole document | objects:write |
| DELETE | /dashboards/{id} | delete; 204 | objects:delete |
| GET | /dashboards/{id}/resolve | the concrete cards and lines to draw | objects:read |
Rendering a resolved dashboard reads values through POST /timeseries/read, which needs timeseries:read.
Status codes: 404 for an unknown id or another workspace's dashboard; 409 when a name is already used in the workspace; 422 for anything the schema rejects, with a detail list whose loc names the field, or a plain detail string for the checks the schema cannot express (an unknown object type, too many lines).
The envelope
GET /dashboards returns {"items": [...]} of summaries, sorted by name. GET /dashboards/{id}, POST, and PUT return the summary plus the document:
{
"id": "c1719097-71d9-468b-a5c1-30fe7ee1ba42",
"name": "Power",
"description": "Forecast against actual production, one chart per site.",
"component_count": 2,
"created_by": {"id": "...", "name": "Henrik", "picture": "https://..."},
"created_at": "2026-10-02T13:10:04Z",
"updated_at": "2026-10-02T13:55:41Z",
"layout": {"columns": 12},
"window": {"start": null, "end": null, "last": "P2D", "ahead": "P2D"},
"components": []
}component_countcounts top-level components (a group counts as one).created_byis the creating user's name and picture, ornullwhen the creator is not a user of the organization (an API key's service account) or is gone.- Documents are stored as written: fields the author did not set are not stored, so a later change of a default or the removal of an optional field never breaks a stored dashboard. The response fills defaults in, so a
GETshows the effective values (width: 6,height: 1,kind: "line", and so on). PUTreplaces the whole document. There is no partial patch; the pattern isGET, edit,PUTback.
Rejected on write, or only a warning
Authors otherwise assume a 201 means everything resolves. Three things are rejected on POST and PUT with a 422:
- the standalone checks on a chart outside a group: every series must name a
record, and no template may refer toitem; - the axis rules: an undeclared axis, two axes on the same side of one pane, a
paneslist that does not cover the panes in use,zero_atoralign_zerocombined withmin; - an unknown object type in a
for_each.
Two things only warn, at resolve time, so the dashboard still saves: records in an iterator that do not exist, and items or cards dropped by a limit. A record that simply lacks a series is reported per line, not as a warning. Resolving and rendering shows where each lands.
The authoring loop
- Fetch
GET /dashboards/schemaonce, so an editor or an agent knows the document. - Write the document. Prefer a
groupover listing record ids, so the dashboard follows the data. Give every group atitleand every chart one;{{ item.name }}is the usual chart title inside a group. POST /dashboards/resolveand readwarnings, every card'serrors, and every line'serror. Fix and repeat.POST /dashboards.- To change it later,
GET, edit,PUTback.
Build a dashboard walks through the loop end to end; The document lists every field.