Errors and rate limits
Error shape
Section titled “Error shape”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 codes
Section titled “Status codes”| 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.
Error codes
Section titled “Error codes”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.
Retrying
Section titled “Retrying”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.
Rate limits
Section titled “Rate limits”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.
Handling 429
Section titled “Handling 429”HTTP/1.1 429 Too Many RequestsRetry-After: 60Content-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.
Staying under the limit
Section titled “Staying under the limit”- Poll
/telemetry/latestat 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
pageSizeproperly. 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.
Pagination
Section titled “Pagination”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.