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,
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable description of what went wrong"
}
}
FieldTypeDescription
successbooleanAlways false for error responses
error.codestringMachine-readable error code (see reference below)
error.messagestringHuman-readable explanation of the error
error.detailsarrayOptional. On VALIDATION_ERROR, lists each invalid property and the constraints it failed

Error responses never include a data field.

Quick Reference​

CodeHTTP StatusWhen it happens
VALIDATION_ERROR400Request body or query params fail field-level validation
INVALID_DATE_RANGE400Date range exceeds the endpoint maximum, end is before start, or a date is not in ISO format
UNAUTHORIZED401Missing or invalid JWT / API Key
TOKEN_EXPIRED401The JWT is valid but has expired (1 hour lifetime)
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
SERVICE_UNAVAILABLE503The user could not be verified at that moment; the token is still valid

Error Codes Reference​

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)
  • An entity ID contains anything other than digits (e.g. ?account_id=ACME, ?task_ids=abc,123)
  • A sort_by value is not accepted by that particular report

How to resolve: Read the error.details array — it lists each invalid property and the constraints it failed, so you know exactly what to fix. A normalizing filter on the server guarantees that every field-validation failure comes back as VALIDATION_ERROR with this details array populated.

Entity IDs must be digits only​

Every GeoTareas entity ID — task, account, client, device, driver, provider, workflow definition, board, card — is a numeric string. Send it as a string (they are too large to survive as JSON numbers), but the string may only contain digits: "982710394857200005".

Any ID-typed parameter that receives a non-numeric value is rejected up front with 400 VALIDATION_ERROR. This applies to single values (?account_id=…), comma-separated lists (?task_ids=…), and paired tuples (?procedence_product_pairs=…).

GET /apidev/v1/tasks?account_id=ACME → 400 VALIDATION_ERROR
GET /apidev/v1/tasks?account_id=51204 → 200 OK

Fields that are not entity IDs keep accepting text: status codes (SA, FIN), your own external references (external_ids, plates, document numbers), free-text search, and enum-style options.

Previously

Before 2026-08-16 a non-numeric ID reached the database and surfaced as 500 INTERNAL_ERROR. It is now a clean 400 with the offending property named in error.details.

sort_by values are validated per report​

The workflow reports each accept their own list of sortable columns. A value outside that list is rejected with 400 VALIDATION_ERROR, and the message names every accepted value for the report you called:

Invalid sort_by 'ZZZ'. Allowed values for this report: totalTasks, slaMetCount, …

Previously an unrecognised sort_by was silently ignored and the report came back in its default order, so a mis-typed column looked like it had worked. Each report page lists its accepted values.

Example response:

{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more fields failed validation.",
"details": [
{
"property": "limit",
"constraints": {
"max": "limit must not be greater than 100"
}
}
]
}
}

INVALID_DATE_RANGE — 400​

Returned when a date range in the request is not acceptable.

When it happens:

  • The range between the start and end dates exceeds the maximum the endpoint allows
  • The end date is earlier than the start date
  • A date is not in ISO format (e.g., 2026-06-13 or 2026-06-13T10:30:00Z)

How to resolve: Shorten the range so it fits within the endpoint's limit, make sure the end date is on or after the start date, and send dates in ISO format.

Example response:

{
"success": false,
"error": {
"code": "INVALID_DATE_RANGE",
"message": "The selected date range is too wide. Choose a shorter period."
}
}

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
  • The user was deactivated, its license or expiration date ended, or it has no permissions left (message User is not active) — checked on every request, not only at login

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,
"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
  • A permission was removed from the user after the token was issued — it takes effect within seconds, without logging in again

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,
"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,
"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,
"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,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again later."
}
}

SERVICE_UNAVAILABLE — 503​

Returned when the server cannot verify the user at that moment (the user and its permissions are checked on every request).

How to resolve: Retry the request after a brief delay using the retry strategy below. Do not log in again: the token is still valid.

Example response:

{
"success": false,
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable. Please retry."
}
}

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 VALIDATION_ERRORRead error.details — it lists each invalid field and the constraint it failed
400 VALIDATION_ERROR on an ID filterThe value must be digits only — you may be sending a name instead of an ID (account_id, country, definition_ids…)
400 VALIDATION_ERROR on sort_byThe column is not sortable in that report — the error message lists the accepted values
400 INVALID_DATE_RANGEIs the range within the endpoint limit? Is the end on or after the start? Are dates in ISO format?
401 TOKEN_EXPIRED after working callsToken expired (1 h TTL) — re-authenticate via /apidev/v1/login
401 UNAUTHORIZED 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
503 SERVICE_UNAVAILABLERetry with backoff — the token is still valid, do not log in again

Retry Strategy​

For transient errors (429, 500 and 503), 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
}
}
}