Every refusal of the management REST API is a JSON body with an HTTP status. This page explains how to read it, which limits exist and how your integration should react.
The error body
Most errors look like this:
{
"message": "The device limit of this account is reached.",
"code": "FORBIDDEN",
"details": { "key": "max_devices", "limit": 43, "current": 43, "adding": 1 }
}
| Field | Meaning |
|---|---|
message | A sentence in English for people. Do not parse it: it may change. |
code | A stable code for the kind of error (table below) |
details | Present when the client can act on more: reason (a stable machine code), key (a plan limit), keys, issues |
Variants:
| Case | Body |
|---|---|
| A body or query that breaks a field rule | 400 { "message": "Validation error", "issues": [{ "path": ["name"], "message": "…" }] } |
| Some routes check the body themselves | 400 { "message": "<the problems, joined with commas>" } |
| Malformed JSON | 400 { "message": "Malformed request body" } |
| An unexpected server error | 500 { "message": "Internal server error" } (the cause is logged on our side, not shown) |
Branch on the status, then on code and details.reason. Each route reference lists its reasons.
Status codes
| Status | Meaning | What to do |
|---|---|---|
| 200, 201 | Done (201: created) | |
| 202 | Accepted, still running (a command) | Read the result later |
| 400 | The request is not valid | Fix the request; do not retry as is |
| 401 | No valid credential: “Invalid or expired API key.” | Check the key; do not retry |
| 403 | The key or its holder may not do this, the key is not allowed from this address, or a plan limit is reached | Check the key’s permissions and the holder’s role; for details.key, the plan |
| 404 | Not found, or not visible to you | Check the id and the key’s permissions |
| 405 | The method is not allowed here (for example writing client attributes) | |
| 409 | A conflict: a duplicate, a record still in use, a state that does not allow the action, or a change by someone else | Read again, then decide |
| 412 | The record changed since you read it (If-Match) | Read again and reapply your change |
| 413 | The body or file is too large | Send less |
| 415 | Wrong Content-Type (MCP servers) | Send application/json |
| 422 | The content breaks a business rule | Fix the content |
| 428 | A required precondition header is missing (If-Match) | Send it |
| 429 | A rate limit | Wait Retry-After seconds, then retry |
| 500 | An error on our side | Retry later with backoff |
| 503 | Temporarily unavailable, for example your organisation is being moved: “This workspace is read-only while it is being moved. Please try again later.” (code TENANT_READ_ONLY, Retry-After: 300) | Retry after Retry-After; reads keep working |
Codes
code | Typical status |
|---|---|
VALIDATION_ERROR | 400, 413, 422, 428 |
UNAUTHORIZED | 401 |
FORBIDDEN | 403 |
FEATURE_NOT_AVAILABLE | 403: your plan does not include the feature |
TENANT_SUSPENDED, TENANT_INACTIVE | 403: your organisation is not active |
TENANT_IP_NOT_ALLOWED | 403: your organisation’s IP rules refuse this address |
NOT_FOUND | 404 |
CONFLICT | 409, 412 |
RATE_LIMIT_EXCEEDED | 429 |
INTERNAL_ERROR | 500, 503 |
Plan limits
A plan limit is a 403 with details.key naming the limit, details.limit its value and details.current your current count, for example max_devices when you create a device. See Usage and quotas and Plans and the IoT add-on.
Rate limits
Several limits apply to every API key call. Each answers 429 when spent.
| Limit | Default | Scope | Message |
|---|---|---|---|
| Address limit | 120 requests a minute, up to 120 at once | Per calling IP address | “Too many requests. Please try again later.” |
| Plan limit | Set by your plan (none if the plan sets none) | Per organisation, all keys and signed-in members together | “Your plan’s API rate limit has been reached. Please try again shortly or upgrade your plan.” |
| Key limit | 1,000 calls per clock minute (Per-Key Rate Limit on the API key settings) | Per key | “API key rate limit exceeded. Please slow down.” |
Some routes add their own:
| Route | Limit |
|---|---|
| Commands | 10 per device per minute (“Too many commands to this device; try again in a minute.”, Retry-After: 60); 600 per organisation per minute (“Too many requests for your plan. Please upgrade or try again later.”, X-RateLimit-Scope: tenant-plan) |
| Device settings writes | 600 per organisation per minute |
| MCP servers | 120 read and 20 write tool calls per credential per minute |
Headers on a 429
| Header | Meaning |
|---|---|
Retry-After | Seconds until a call will be admitted again |
RateLimit-Limit, X-RateLimit-Limit | The limit per minute |
RateLimit-Remaining, X-RateLimit-Remaining | Calls left (0) |
RateLimit-Reset | Seconds until the bucket is full again |
Not every 429 carries every header. If Retry-After is missing, wait 60 seconds.
The address limit refills continuously: one call comes back every half second, so a steady 2 calls a second never meets it. Spread bursts rather than sending them at once.
Retrying safely
- Retry only
429,500,502,503and504, and network errors. - Wait
Retry-Afterwhen present; otherwise back off exponentially (1, 2, 4, 8 … seconds, up to a minute) with some random jitter. - Give up after a few attempts and log the last body.
- Before you retry a
POSTthat creates something (a device, a command), check whether the first call took effect: list or read first. A commandPOSThas no idempotency key. - Never retry
400,401,403,404or422unchanged.
async function call(url, init, tries = 5) {
for (let i = 0; ; i++) {
const res = await fetch(url, init);
if (res.ok || i === tries - 1 || ![429, 500, 502, 503, 504].includes(res.status)) return res;
const wait = Number(res.headers.get('Retry-After')) || Math.min(60, 2 ** i) + Math.random();
await new Promise((r) => setTimeout(r, wait * 1000));
}
}
Errors you may meet first
| You see | Likely cause |
|---|---|
401 on every call | A typo in the key, an expired or revoked key, or a key of another organisation (the tenantId in the path must be the key’s) |
403 “API key does not have the required permission.” | The key lacks the route’s permission. Edit the key’s Module permissions. |
403 “You are not permitted to perform this action” | The key holds the permission, but its holder no longer does (role changed, group removed, access policy) |
403 “This API key may not be used from the current IP address.” | The key’s IP allowlist, or the organisation’s default list |
404 for a record you can see in the app | The key’s holder sees it through a permission the key does not hold, or the id is of another organisation |
429 from one server only | The address limit: spread calls or use fewer, larger pages |

