Error Handling
All API errors follow a consistent envelope structure, making it straightforward to detect failures and react programmatically.
Error Envelope
Every error response uses this format:
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description of what went wrong"
}
}
| Field | Type | Description |
|---|---|---|
success | boolean | Always false for error responses |
data | null | Always null when an error occurs |
error.code | string | Machine-readable error code (see reference below) |
error.message | string | Human-readable explanation of the error |
Quick Reference
| Code | HTTP Status | When it happens |
|---|---|---|
BAD_REQUEST | 400 | Missing or malformed headers, invalid request structure |
VALIDATION_ERROR | 400 | Request body or query params fail field-level validation |
UNAUTHORIZED | 401 | Missing, expired, or invalid JWT / API Key |
FORBIDDEN | 403 | Authenticated but insufficient permissions for this endpoint |
NOT_FOUND | 404 | Resource ID doesn't exist or URL path is incorrect |
RATE_LIMITED | 429 | Too many requests within the rate limit window |
INTERNAL_ERROR | 500 | Unexpected server-side failure |
Error Codes Reference
BAD_REQUEST — 400
Returned when the request structure is invalid before field-level validation occurs.
When it happens:
- The
tenantheader is missing or empty - The
Content-Typeheader is missing on POST/PUT requests - The request body is not valid JSON
How to resolve: Check that all required headers are present and that the request body is well-formed JSON.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "BAD_REQUEST",
"message": "The tenant header is required."
}
}
VALIDATION_ERROR — 400
Returned when the request body or query parameters fail field-level validation.
When it happens:
- A required field is missing
- A field has the wrong type (e.g., string instead of number)
- A value is out of the allowed range (e.g.,
limitgreater than 100)
How to resolve:
Check the error.message field for details about which field failed validation and why.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "\"limit\" must be a number between 1 and 100"
}
}
UNAUTHORIZED — 401
Returned when the request lacks valid authentication credentials.
When it happens:
- The
Authorizationheader is missing or malformed - The JWT token has expired (1 hour TTL)
- The
X-API-Keyheader is missing or invalid - The
tenantheader does not match the token's tenant - The API Key is inactive or outside its validity window
How to resolve: Re-authenticate via the Login endpoint to obtain a fresh token. If the API Key is rejected, verify its status and validity window with your account manager.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "UNAUTHORIZED",
"message": "Token has expired. Please re-authenticate."
}
}
FORBIDDEN — 403
Returned when the user is authenticated but does not have permission to access the requested resource.
When it happens:
- The user's role does not include the required permission for this endpoint
- The resource belongs to a scope the user cannot access
How to resolve: Contact your administrator to verify that the user account has the required permissions assigned. Each endpoint documents its required permission in the API reference.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "FORBIDDEN",
"message": "Insufficient permissions to access this resource."
}
}
NOT_FOUND — 404
Returned when the requested resource does not exist.
When it happens:
- The ID in the URL does not match any existing record
- The resource was deleted
- The URL path is incorrect
How to resolve: Verify that the resource ID is correct and that the resource has not been deleted. Double-check the endpoint URL.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "NOT_FOUND",
"message": "Device with ID 99999 not found"
}
}
RATE_LIMITED — 429
Returned when the request exceeds the allowed rate limit for the endpoint.
When it happens:
- Too many requests sent within the rate limit window
- Telemetry token bucket is exhausted
How to resolve:
Wait until the time indicated by the Retry-After response header before retrying. See Rate Limits for details on limits per endpoint group.
Example response:
{
"success": false,
"data": null,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 12 seconds."
}
}
INTERNAL_ERROR — 500
Returned when an unexpected server-side error occurs.
When it happens:
- An unhandled exception on the server
- A downstream service is temporarily unavailable
How to resolve:
Retry the request after a brief delay using the retry strategy below. If the error persists, contact support and include the X-Request-Id value from the response headers (see below).
Example response:
{
"success": false,
"data": null,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later."
}
}
X-Request-Id
Every API response includes an X-Request-Id header — a unique identifier generated by the server for that specific request. You don't need to send it; the server creates it automatically.
HTTP/1.1 500 Internal Server Error
X-Request-Id: req_a1b2c3d4e5f6
When contacting support about persistent errors, always include this value. It allows the team to trace the exact request through server logs.
Troubleshooting
| I'm getting... | Check first |
|---|---|
400 BAD_REQUEST | Are all required headers present? (tenant, Content-Type on POST/PUT) |
400 VALIDATION_ERROR | Read error.message — it names the exact field and constraint that failed |
401 after working calls | Token expired (1 h TTL) — re-authenticate via /apidev/v1/login |
401 on first call | Is X-API-Key present? Does tenant match the JWT's tenant? |
403 on a valid endpoint | User role lacks the required permission — check with your admin |
404 with a correct ID | Resource may have been deleted, or the URL path has a typo |
429 in a loop | Stop retrying — respect Retry-After header, implement backoff |
500 once | Retry with backoff. Transient failures happen |
500 repeatedly | Stop retrying, contact support with X-Request-Id |
Retry Strategy
For transient errors (429 and 500), implement an exponential backoff strategy:
- First retry: wait 1 second
- Second retry: wait 2 seconds
- Third retry: wait 4 seconds
- Fourth retry: wait 8 seconds
- Give up after 4 retries and log the error
For 429 responses, always prefer the Retry-After header value over your own backoff calculation — it gives the exact wait time needed.
- JavaScript
- Python
async function requestWithRetry(fn, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const status = error.response?.status;
if (status === 429 || status === 500) {
if (attempt === maxRetries) throw error;
const retryAfter = error.response?.headers?.['retry-after'];
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error; // Non-retryable error
}
}
}
import time
import requests
def request_with_retry(fn, max_retries=4):
for attempt in range(max_retries + 1):
response = fn()
if response.status_code in (429, 500):
if attempt == max_retries:
response.raise_for_status()
retry_after = response.headers.get("Retry-After")
delay = int(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
continue
return response
# Usage
def call_api():
return requests.get(
f"https://{TENANT}/apidev/v1/fleet/devices",
headers=headers,
params={"limit": 25},
)
response = request_with_retry(call_api)