Skip to content

Errors and rate limits

Every error response is an RFC 7807 problem document, served as application/problem+json.

{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "interval=15m supports a maximum range of 31 days.",
"instance": "/api/v1/external/systems/1234567890/telemetry/energy",
"errorCode": "ValidationFailed",
"correlationId": "3f8c1b7e-24a9-4d05-b6f3-9e7a2c40d158"
}
Field Use it for
status The HTTP status, repeated in the body
title A short, stable summary of the class of problem
detail A human-readable explanation of this specific failure. Log it; do not parse it
instance The request path
errorCode The machine-readable code. Branch on this, not on title or detail
correlationId The id of this request in our logs. Include it in every support mail

detail is written for a person and its wording can change at any time. errorCode and status are the contract.

Status Meaning What to do
400 The request is malformed or a parameter is out of range Fix the request. Retrying will not help
401 No API key, or a key that is unknown, revoked or expired Check the key. See Authentication
403 Valid key, but it lacks the scope the endpoint requires Create a key with that scope
404 No such resource for this company. Also returned for a resource that exists but belongs to another company Check the serial number or id
429 Rate limit exceeded Back off; see below
500 Something failed on our side Retry with backoff. If it persists, send us the correlationId
503 A dependency is temporarily unavailable Retry with backoff

404 never distinguishes “does not exist” from “not yours”. That is deliberate: a 403 would confirm that a serial number exists, which would let anyone with a key enumerate the fleet.

The codes you will meet on /api/v1/external/*:

errorCode Status Meaning
ValidationFailed 400 A parameter is missing, malformed or out of range. detail names the parameter
NotFound 404 No such system, alert or subscription for this company
Forbidden 403 The key does not carry the required scope

Platform-wide codes can also surface, mostly from the layers in front of the endpoints:

errorCode Status Meaning
RATE_LIMIT_EXCEEDED 429 Too many requests for this key
AUTH_INVALID_CREDENTIALS 401 The credential was not accepted
AUTH_TOKEN_EXPIRED 401 The credential has expired
AUTH_INSUFFICIENT_SCOPE 403 The credential lacks the required scope
VALIDATION_FAILED 400 Malformed request
RESOURCE_NOT_FOUND 404 No such resource
RESOURCE_CONFLICT 409 The request conflicts with the current state
SYSTEM_INTERNAL_ERROR 500 Unhandled failure on our side
SYSTEM_SERVICE_UNAVAILABLE 503 A dependency is unavailable

Treat an errorCode you do not recognise as “an error of this status class” and fall back to status. New codes can be introduced in a non-breaking release.

  • 400, 401, 403, 404 - do not retry. The same request will fail the same way.
  • 429 - retry after the interval below.
  • 500, 503 - retry with exponential backoff and jitter, a handful of times, then give up and alert your own on-call.

Limits are applied per API key, in a sliding window:

Limit 600 requests
Window 60 seconds, sliding
Partition The individual key, not the company

Because the partition is the key, one busy integration cannot exhaust the budget of another. It also means splitting your traffic across several keys does not raise your total allowance in any principled way - if you need more, talk to us.

Successful responses carry:

Header Meaning
RateLimit-Limit Requests permitted in the window
RateLimit-Remaining Requests still available in the current window

Treat RateLimit-Remaining as advisory. It is a snapshot taken as the response is written, and it can be absent on a request that did not reach the limiter.

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json
{
"type": "https://httpstatuses.io/429",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 60 seconds.",
"instance": "/api/v1/external/systems",
"errorCode": "RATE_LIMIT_EXCEEDED",
"correlationId": "",
"retryAfterSeconds": 60
}

Wait Retry-After seconds (the same value is in retryAfterSeconds) and try again. Do not retry immediately, and do not retry in a tight loop across many workers - add jitter so your fleet does not synchronise.

  • Poll /telemetry/latest at most once a minute per system. Anything faster returns the same numbers.
  • Poll the series endpoints at most every 15 minutes. The underlying aggregation does not refresh faster than that.
  • Use webhooks instead of polling /alerts. One subscription replaces a poll loop across your whole fleet.
  • Use pageSize properly. One request for 200 systems beats four for 50.

At 600 requests a minute you can refresh a fleet of 500 systems roughly once a minute, which is more often than the data changes.

List endpoints share one convention.

Request:

Parameter Default Notes
page 1 1-based
pageSize 50 Maximum 200

Response:

{
"items": [],
"page": 1,
"pageSize": 50,
"totalCount": 137
}

totalCount is the number of matching records across all pages, not the number in items. The last page is reached when page * pageSize >= totalCount, or simply when items comes back shorter than pageSize.

A page beyond the end returns an empty items array, not a 404.

Records can be added between two page requests, which shifts everything by one. For a consistent sweep of alerts, prefer filtering by since over paging deep.

The telemetry series endpoints are not paginated. They are bounded by the range limits in Intervals and bucket boundaries instead.