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": "..."
}
FieldMeaning
statusHTTP status code, repeated in the body.
codeMachine-readable error code, for example NOT_FOUND, UNPROCESSABLE_CONTENT, TOO_MANY_REQUESTS. Some errors return a lowercase, more specific code. Match on status first.
messageHuman-readable description.
definedtrue for errors the endpoint documents, false for unexpected ones.
dataOptional extra detail.
requestIdInclude 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

StatusMeaning
200Success.
401Missing, invalid, or expired token.
403Token lacks the required permission.
404Resource not found. On list endpoints, also returned for an empty result (see below).
422Validation failed.
429Rate limit exceeded. The body is plain text; see Rate limits.
500Unexpected server error. Retry with backoff; include the requestId if it persists.
503Service temporarily unavailable. Retry with backoff.

Pagination

List endpoints accept:

ParameterTypeDefaultNotes
pageinteger >= 11
per-pageinteger 1 to 100010

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.

ElementAllowed values
operatoreq, not-eq, lt, lte, gt, gte, in, not-in, like, not-like, ilike, not-ilike, between, not-between
typestring, int, float, boolean, datetime, null
valuestring, 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):

EndpointKeys
/buildingsnanoid, oldid, name, slug, address, timezone, floorArea, industry, mainsSupplyFrequency, createdAt, updatedAt
/panelsnanoid, oldid, name, notes, createdAt, updatedAt
/breakersnanoid, oldid, name, size, active, createdAt, updatedAt
/data-gatewaysid, 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

IdentifierFormatUse
nanoidPrefixed string: building_..., panel_..., breaker_..., circuit_...Primary identifier in v3 paths.
slugStringBuildings only. Accepted in place of the NanoID in /buildings/{buildingIdOrSlug}.
oldidIntegerIdentifier 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.


Did this page help you?