Errors and rate limiting
All errors return a JSON body with a machine-readable error code and a human-readable
message. Use the error code in your error-handling logic — the message
may change, the code will not.
Error response format
{
"error": "invalid_credentials",
"message": "The API key or secret is incorrect.",
"requestId": "req_01HZ9VBKX4EFGH2J..."
}
requestId when contacting MoovLogic support. It uniquely identifies the failed request in our logs.HTTP status codes
| Status | Meaning |
|---|---|
| 200 OK | Request succeeded. |
| 201 Created | Booking was created successfully. |
| 400 Bad Request | Validation error. Check the errors array in the response. |
| 401 Unauthorized | Token is missing, expired, or invalid. |
| 403 Forbidden | Token is valid but you do not have permission for this operation. |
| 404 Not Found | The requested booking tagName or service type ID does not exist. |
| 422 Unprocessable Entity | Business rule violation — for example, an invalid status transition or a pickup time in the past. |
| 429 Too Many Requests | Rate limit exceeded. See Rate limits below. |
| 500 Internal Server Error | Unexpected server error. Retry with exponential back-off. If it persists, contact support with the requestId. |
Error codes reference
| Error code | HTTP status | Description |
|---|---|---|
| invalid_credentials | 401 | The API key or secret is incorrect. |
| token_expired | 401 | The JWT has expired. Re-authenticate to get a new token. |
| token_missing | 401 | The Authorization header is absent from the request. |
| insufficient_permission | 403 | Your service account does not have access to this resource or operation. |
| booking_not_found | 404 | No booking exists with this tagName, or it belongs to a different account. |
| service_type_not_found | 404 | The serviceTypeId does not exist or is not active. |
| no_pricing_available | 422 | No active tariff is configured for this route and service type combination. |
| pickup_in_past | 422 | The pickupDateTimeUtc is in the past. |
| invalid_status_transition | 422 | The requested status change is not a valid transition from the current status. |
| booking_not_cancellable | 422 | The booking cannot be cancelled at its current status (e.g. already completed or cancelled). |
| validation_error | 400 | One or more fields failed validation. See the errors array for field-level detail. |
| rate_limit_exceeded | 429 | You have exceeded the request rate limit for this endpoint. |
Validation errors
When error is validation_error, the response includes an errors array with field-level detail:
{
"error": "validation_error",
"message": "One or more fields failed validation.",
"errors": [
{
"field": "pickupDateTimeUtc",
"message": "Pickup time must be in the future."
},
{
"field": "passengerEmail",
"message": "Invalid email address format."
}
],
"requestId": "req_01HZ9VBKX4E..."
}
Rate limits
| Policy | Limit | Applies to |
|---|---|---|
| AuthPolicy | 10 requests / minute | POST /api/v1/auth/token |
| ApiPolicy | 60 requests / minute | All other endpoints |
Rate limit information is returned in the response headers on every request:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714521600
| Header | Description |
|---|---|
| X-RateLimit-Limit | The maximum requests allowed in the current window. |
| X-RateLimit-Remaining | The number of requests remaining in the current window. |
| X-RateLimit-Reset | Unix timestamp indicating when the rate limit window resets. |
X-RateLimit-Remaining and pause when it approaches 0. X-RateLimit-Reset tells you exactly when the window resets so you can resume immediately rather than guessing.Retry strategy
400, 401, 403, or 422 responses — these indicate problems with your request that won't resolve on their own. Fix the underlying issue before retrying.For 429 and 5xx responses, retry with exponential back-off:
import time, requests
def api_request_with_retry(method, url, **kwargs):
max_attempts = 5
base_delay = 1.0
for attempt in range(max_attempts):
response = requests.request(method, url, **kwargs)
if response.status_code not in (429, 500, 502, 503, 504):
return response
if attempt < max_attempts - 1:
wait = base_delay * (2 ** attempt)
time.sleep(wait)
raise Exception(f"Request failed after {max_attempts} attempts")
async function apiRequestWithRetry(url, options, maxAttempts = 5) {
const retryable = new Set([429, 500, 502, 503, 504]);
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(url, options);
if (!retryable.has(res.status)) return res;
if (attempt < maxAttempts - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
throw new Error(`Request failed after ${maxAttempts} attempts`);
}
Getting help
If a request fails unexpectedly and retrying does not resolve it, contact api-support@moovlogic.com with:
- The
requestIdfrom the error response - The endpoint and HTTP method
- The timestamp of the failed request