Skip to main content

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"
}
}
FieldTypeDescription
successbooleanAlways false for error responses
datanullAlways null when an error occurs
error.codestringMachine-readable error code (see reference below)
error.messagestringHuman-readable explanation of the error

Quick Reference

CodeHTTP StatusWhen it happens
BAD_REQUEST400Missing or malformed headers, invalid request structure
VALIDATION_ERROR400Request body or query params fail field-level validation
UNAUTHORIZED401Missing, expired, or invalid JWT / API Key
FORBIDDEN403Authenticated but insufficient permissions for this endpoint
NOT_FOUND404Resource ID doesn't exist or URL path is incorrect
RATE_LIMITED429Too many requests within the rate limit window
INTERNAL_ERROR500Unexpected 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 tenant header is missing or empty
  • The Content-Type header 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., limit greater 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 Authorization header is missing or malformed
  • The JWT token has expired (1 hour TTL)
  • The X-API-Key header is missing or invalid
  • The tenant header 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_REQUESTAre all required headers present? (tenant, Content-Type on POST/PUT)
400 VALIDATION_ERRORRead error.message — it names the exact field and constraint that failed
401 after working callsToken expired (1 h TTL) — re-authenticate via /apidev/v1/login
401 on first callIs X-API-Key present? Does tenant match the JWT's tenant?
403 on a valid endpointUser role lacks the required permission — check with your admin
404 with a correct IDResource may have been deleted, or the URL path has a typo
429 in a loopStop retrying — respect Retry-After header, implement backoff
500 onceRetry with backoff. Transient failures happen
500 repeatedlyStop retrying, contact support with X-Request-Id

Retry Strategy

For transient errors (429 and 500), implement an exponential backoff strategy:

  1. First retry: wait 1 second
  2. Second retry: wait 2 seconds
  3. Third retry: wait 4 seconds
  4. Fourth retry: wait 8 seconds
  5. 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.

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
}
}
}