Skip to content

Error Handling

All GoTab API errors return a JSON body. The shape is consistent across REST endpoints:

{
"error": "invalid_token",
"message": "The access token has expired.",
"statusCode": 401
}

Some validation errors (422) include field-level detail:

{
"error": "validation_failed",
"message": "Request validation failed.",
"statusCode": 422,
"details": [
{ "field": "api_access_id", "message": "Required" },
{ "field": "grant_type", "message": "Must be one of: authorization_code, refresh_token" }
]
}

GraphQL errors follow the standard GraphQL error envelope:

{
"data": null,
"errors": [
{
"message": "Unauthorized",
"extensions": { "code": "UNAUTHENTICATED" }
}
]
}

StatusMeaning in GoTab context
400 Bad RequestMalformed request — missing required fields or invalid JSON
401 UnauthorizedMissing, expired, or revoked Bearer token
403 ForbiddenValid token but insufficient permissions for this resource
404 Not FoundResource doesn’t exist, or the endpoint path is wrong
409 ConflictRequest conflicts with existing state (e.g. duplicate creation)
422 Unprocessable EntityRequest is well-formed but fails validation — check details
429 Too Many RequestsRate limit exceeded — see Rate Limits
500 Internal Server ErrorGoTab-side error — safe to retry with backoff

401 — Token expired or revoked

{ "error": "invalid_token", "message": "The access token has expired.", "statusCode": 401 }

Action: refresh the token using your refresh_token and retry the request. See OAuth Flows for the refresh call.

401 — Missing Authorization header

{ "error": "unauthorized", "message": "No authorization token provided.", "statusCode": 401 }

Action: ensure every request includes Authorization: Bearer YOUR_TOKEN. See Authentication.

403 — Insufficient permissions

{ "error": "forbidden", "message": "Your credentials do not have access to this resource.", "statusCode": 403 }

Action: verify the location has authorized your integration. Do not retry without re-authorizing.


When you exceed the rate limit, the API returns:

{ "error": "rate_limit_exceeded", "message": "Too many requests.", "statusCode": 429 }

The response includes a Retry-After header indicating how many seconds to wait:

HTTP/1.1 429 Too Many Requests
Retry-After: 15

Wait the indicated time before retrying. For proactive management, see Rate Limits.


Validation errors include a details array that identifies which fields failed and why:

{
"error": "validation_failed",
"statusCode": 422,
"details": [
{ "field": "locationUuid", "message": "Invalid UUID format" },
{ "field": "total", "message": "Must be a positive number" }
]
}

These are not retryable — fix the request data before retrying.


Retryable errors: 429, 500, 502, 503, 504, and network timeouts.

Non-retryable errors: 400, 401 (until token is refreshed), 403, 404, 409, 422.

For retryable errors, wait progressively longer between attempts:

async function fetchWithRetry(url, options, maxRetries = 4) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res.json();
// Non-retryable
if ([400, 403, 404, 409, 422].includes(res.status)) {
throw new Error(`Non-retryable error: ${res.status}`);
}
// Respect Retry-After on 429
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '15', 10);
await sleep(retryAfter * 1000);
continue;
}
// Exponential backoff for 5xx and last attempt
if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} retries`);
await sleep(Math.min(1000 * 2 ** attempt, 30_000));
}
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));