Errors and conventions
Status codes, filter and sort syntax, and the paging quirks that trip people up.
This page describes conventions shared by the v3 endpoints: error responses, status codes, pagination, filtering, sorting, and identifiers. For the Data API v4 error format, see Pull time-series data.
Authentication and scopes
Send Authorization: Bearer <access_token> on every request. Each endpoint requires a permission on your token, for example read:buildings, read:panels, read:breakers, or read:data-gateways. A token without the right permission receives 403.
Error responses
Errors produced by the endpoints are JSON objects:
{
"defined": true,
"code": "NOT_FOUND",
"status": 404,
"message": "Building with slug 'example' not found.",
"requestId": "..."
}| Field | Meaning |
|---|---|
status | HTTP status code, repeated in the body. |
code | Machine-readable error code, for example NOT_FOUND, UNPROCESSABLE_CONTENT, TOO_MANY_REQUESTS. Some errors return a lowercase, more specific code. Match on status first. |
message | Human-readable description. |
defined | true for errors the endpoint documents, false for unexpected ones. |
data | Optional extra detail. |
requestId | Include this when you contact support. |
Authentication failures (401) and permission failures (403) can also be returned as an object with name, message, status and requestId. Parse defensively and rely on the HTTP status.
Validation errors (422)
Invalid query, path, or body input returns 422 with code failed_validation. The offending part of the request is named in query, body, headers or pathParams, each holding { "name": "ZodError", "issues": [...] }; parts that were valid are null.
Status codes
| Status | Meaning |
|---|---|
| 200 | Success. |
| 401 | Missing, invalid, or expired token. |
| 403 | Token lacks the required permission. |
| 404 | Resource not found. On list endpoints, also returned for an empty result (see below). |
| 422 | Validation failed. |
| 429 | Rate limit exceeded. The body is plain text; see Rate limits. |
| 500 | Unexpected server error. Retry with backoff; include the requestId if it persists. |
| 503 | Service temporarily unavailable. Retry with backoff. |
Pagination
List endpoints accept:
| Parameter | Type | Default | Notes |
|---|---|---|---|
page | integer >= 1 | 1 | |
per-page | integer 1 to 1000 | 10 |
and return currentPage, pages, total, plus an array named for the resource (buildings, panels, breakers, dataGateways). Iterate until currentPage >= pages. Results are ordered newest first by creation date unless you supply sort.
A page number beyond the last page returns 200 with an empty array (not 404), for example { "currentPage": 9999, "pages": 186, "total": 371, "buildings": [] }.
Empty lists: when no items match (including when filter references an unknown key), the API returns HTTP 404 with an empty page body, {"currentPage": 1, "pages": 1, "total": 0, "<resource>": []}. Handle this as an empty result. Only treat a 404 as "empty" when the body has total: 0. Any other 404 (a wrong path, or an unknown ID, which has code: NOT_FOUND) is a real error, and silently treating it as empty would leave your data set empty without telling you.
Filtering
filter is a JSON-encoded value in the query string. Each clause has key, operator, type, and value.
| Element | Allowed values |
|---|---|
operator | eq, not-eq, lt, lte, gt, gte, in, not-in, like, not-like, ilike, not-ilike, between, not-between |
type | string, int, float, boolean, datetime, null |
value | string, number, boolean, date, null, or an array of one of those (for in, between, and similar) |
Single clause:
filter={"key":"name","operator":"ilike","type":"string","value":"main"}
Several clauses as a single JSON array, combined with filter-operator=and (default) or filter-operator=or:
filter=[{"key":"name","operator":"like","type":"string","value":"A"},{"key":"oldid","operator":"gte","type":"int","value":100}]&filter-operator=or
Nested groups:
filter={"operator":"or","clauses":[{"key":"oldid","operator":"eq","type":"int","value":1},{"key":"name","operator":"like","type":"string","value":"test"}]}
URL-encode the JSON. With curl, use -G --data-urlencode 'filter=...'. like and ilike add the % wildcards themselves, so pass the bare value (main, not %main%). type is required and is one of string, int, float, boolean, datetime or null. Operators: eq, not-eq, lt, lte, gt, gte, in, not-in, like, not-like, ilike, not-ilike, between, not-between. Groups can be nested up to 3 levels.
Filterable keys (the same keys can be used in sort):
| Endpoint | Keys |
|---|---|
/buildings | nanoid, oldid, name, slug, address, timezone, floorArea, industry, mainsSupplyFrequency, createdAt, updatedAt |
/panels | nanoid, oldid, name, notes, createdAt, updatedAt |
/breakers | nanoid, oldid, name, size, active, createdAt, updatedAt |
/data-gateways | id, serialNumber, panelId, panelName, buildingId, buildingName, accountId, accountName, lifecycle, hardwareVersion, deviceModel, lastDataTimestamp, dataLatencyMinutes, dataUptime24H, isConnected, lastConnectedAt, and activeIpAddress (filter only) |
Sorting
sort is a JSON object or array of objects with key and desc (default false). Earlier entries take priority.
sort=[{"key":"name","desc":false}]
An unknown sort key returns 422 with { "defined": true, "code": "UNPROCESSABLE_CONTENT", "status": 422, "message": "Resource key \"nokey\" not found", ... }.
Identifiers
| Identifier | Format | Use |
|---|---|---|
nanoid | Prefixed string: building_..., panel_..., breaker_..., circuit_... | Primary identifier in v3 paths. |
slug | String | Buildings only. Accepted in place of the NanoID in /buildings/{buildingIdOrSlug}. |
oldid | Integer | Identifier used by Data API v4. |
Timestamps
createdAt and updatedAt are ISO 8601 date-time strings.
CORS
Browser requests from any origin are accepted by the API, but do not embed client secrets in browser code.
Updated 27 days ago

