Conventions
Pagination, filtering, naming, PATCH semantics, errors, permissions, and other rules that apply across the API.
The API is small enough that a few conventions cover most of it. They hold for every object type, built in or custom.
Pagination
List endpoints return up to limit records (default 50, max 200) in items.
r = requests.get(f"{BASE}/objects/sites", headers=HEADERS, params={"limit": 200})
r.raise_for_status()
sites = r.json()["items"]When a workspace holds more records than one page, the response's next_cursor is set; pass it back as ?cursor= to fetch the next page, until it comes back null. For totals that do not depend on paging, GET /objects/{object_name}/stats answers in one call, and GET /objects/{object_name}/export streams the whole type. See Bulk record reads.
Narrowing a list
?filter[<attribute>]=valuekeeps only records whose attribute has that value. Repeat it for several values of one attribute (OR-ed) or several attributes (AND-ed). Filters work on custom types, whose stored fields are exactly their attribute slugs; on the built-in types a filter is a400. Anobjectattribute filters by the target's id, and abetweenattribute by either end or one position: see Linking records.?fields=a,btrimsvaluesto the named attributes. The envelope always rides along.?parent_id=<id>lists the parts of one owner.
Slugs and field names
For slugs you choose, such as a timeseries type, a custom object type name, or an attribute slug, prefer kebab-case: power-production, not power_production.
Field names in request and response bodies (valid_time, capacity_kw, next_cursor) are fixed by the API and are snake_case.
Record names
name is required on every record, with one exception: a record of a type with a between attribute is identified by the two records it runs between, and a name only tells parallel records apart. Names may not contain / and must be unique among siblings: parts under one owner, and top-level records (sites, custom records) within the workspace. Creating a record whose name conflicts returns 409.
PATCH semantics
Only fields present in the body are applied, values included: send the attributes you change and leave the rest out.
Two attribute kinds behave differently, and it pays to know which is which:
- Part lists are additive. On a site,
values.assetsin a PATCH creates a new asset for each entry and never removes one, so a forgotten field can never delete a part. Modify or delete individual assets through the asset routes. - Reference lists are replaced. A PATCH on an
objectsattribute withrelation: referencewrites the whole set; what it leaves out is unlinked. The records on the far end are untouched either way.
Errors
| Status | When |
|---|---|
400 | A filter on a built-in type, an aggregation parameter on a history read, or an operation the attribute kind does not allow (unlinking a part). |
401 | No key, or an invalid one. |
403 | Missing permission. Also returned when trying to modify or delete a built-in object type. |
404 | Not found. Records in another workspace simply do not resolve, so they look the same as records that do not exist. |
409 | Conflict: a duplicate name, a link cycle, a second record between the same pair of ends, or deleting an object type that still has records or is still targeted by another type. |
422 | Validation error in the body or query parameters. |
Error bodies have the shape {"detail": "..."}.
Stats
Every record carries a stats object with numbers the platform computes; you cannot set them. On a site, stats.total_capacity_kw is the sum of its assets' capacity_kw and updates as assets change. Whole-type totals live at GET /objects/{object_name}/stats.
Timestamps
Timestamps out are ISO 8601 in UTC (2026-09-22T10:00:00Z). Timestamps in may carry any offset; a naive timestamp is read as UTC. Timeseries windows (start, end) are inclusive on both ends.
Compression
Responses over a kilobyte are gzip-compressed when the request says Accept-Encoding: gzip. Python requests does this by default and decompresses transparently. Timeseries payloads shrink about tenfold, so leave it on.
Permissions
A key restricted at creation needs the permission matching the call, and sees 403 otherwise.
| Permission | Allows |
|---|---|
objects:read | Listing and reading records, object types, links, stats, exports. |
objects:write | Creating and updating records and object types, linking and unlinking. |
objects:delete | Deleting records and object types. |
timeseries:read | Reading series, single or batch. |
timeseries:write | Uploading points, and deleting them (a deletion appends a retraction, so it is a write). |
Retry safety
Several operations are designed so that a retried request is harmless:
- Timeseries uploads dedup by default. Re-sending points the store already holds writes nothing (they count as
points_skipped). A changed value becomes the current one, and the previous value stays in the series' history. - Batch writes land whole or not at all. If any entry of
POST /timeseries/writefails validation, nothing is written. - Deleting points is a retraction. Points that are already gone count as
points_missingand change nothing. - Linking is idempotent.
PUT .../{attribute}/{target_id}on a record that is already linked returns200with the unchanged record. - Creating a record with nested parts is all or nothing. If any part fails, no record is created.