Errors
HTTP status codes, the error envelope, and the machine codes Routal returns.
The Routal API uses standard HTTP status codes. Every non-2xx response body is JSON with the same shape.
Error envelope
Every error response uses the same schema, named ApiError in the OpenAPI spec:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | Error message |
message_id | string | yes | Error id |
Example body:
{
"message": "Domain not found",
"message_id": "highway.domain.error.not_found"
}messageis human-readable, suitable for log lines but not for end-user display. Wording can change for clarity across releases.message_idis the stable, dotted machine code. Use it for branching logic in your integration.
message_id follows the pattern highway.<context>.error.<reason>. highway is Routal's stable namespace for machine-readable error codes — branch on the full string, not on substrings.
HTTP status codes
| Status | Meaning | What to do |
|---|---|---|
200 OK | Request succeeded. | Process the response body. |
201 Created | Resource created. | Read the new resource's id from the response body. |
204 No Content | Success with no body (typical for DELETE). | Treat as success. |
400 Bad Request | Payload validation failed — missing field, wrong type, unknown enum value, invalid ID format. | Inspect message_id. Fix the request. Do not retry the same payload — it will fail again. |
401 Unauthorized | Missing or invalid private_key. | Verify the key. See Authentication. |
403 Forbidden | Authenticated, but the credential does not have permission for this resource or action. | Check project membership and the key's organization. |
404 Not Found | Resource ID does not exist (or has been deleted). | Confirm the ID. Do not retry. |
409 Conflict | The request collides with current resource state (e.g. deleting a locked route). | Read current state, then decide. |
429 Too Many Requests | You've hit a rate limit. | Back off and retry. |
5xx Server Error | Something on our side went wrong. | Retry with exponential backoff. Check status.routal.com. If persistent, email support with the message_id. |
Common machine codes
Routal emits dozens of message_id values across many domains. Below are the most common ones an integration tends to hit. Codes you don't see here exist — when in doubt, log the message_id you got back and contact support.
Authentication
message_id | Meaning |
|---|---|
highway.apiKey.error.not_found | The private_key does not match any active API key in the tenant. |
There is no separate missing_key / invalid_key / revoked_key taxonomy — every API-key auth failure surfaces as highway.apiKey.error.not_found.
Resource not found
message_id | Meaning |
|---|---|
highway.plan.error.not_found | No plan with the given id. |
highway.route.error.not_found | No route with the given id. |
highway.stop.error.not_found | No stop with the given stop_id. |
highway.task.error.not_found | No task with the given task_id. |
highway.vehicle.error.not_found | No vehicle with the given vehicle_id. |
Validation
message_id | Meaning |
|---|---|
highway.validation.error.invalid_id | A required ID field was not a valid 24-character hex string. |
highway.stop.error.custom_fields_invalid | A stop's custom_fields did not match the project's custom-field definitions. |
highway.route.error.custom_fields_invalid | Same, for a route. |
highway.vehicle.error.custom_fields_invalid | Same, for a vehicle. |
highway.geocoding.error.wrong_lat_lng | A location's lat / lng is outside the valid range. |
Some payload validation errors (missing required field, wrong type) surface as a plain 400 with a different body shape — no message_id, just a statusCode and a message describing the offending field:
{ "statusCode": 400, "error": "Bad Request", "message": "child \"label\" fails because [...]" }Treat statusCode === 400 && !body.message_id as the sentinel for this case. Read message to figure out which field is wrong, fix the request, and don't retry until you've fixed it.
Permissions
message_id | Meaning |
|---|---|
highway.plan.error.user_not_allowed | Authenticated, but the user/key lacks permission on the target plan (or its project). |
highway.organization.error.user_not_allowed | The user is not part of the target organization. |
highway.user.error.user_not_allowed | The action is not permitted for this user. |
Resource state
message_id | Meaning |
|---|---|
highway.route.error.locked | Cannot delete or modify a locked route (is_locked: true). |
highway.route.error.not_in_transit | The action requires the route to be in_transit. |
highway.stop.error.move_not_pending_stops | Cannot move stops that are no longer pending. |
Optimization
message_id | Meaning |
|---|---|
highway.optimization.error.sync_optimization_already_progress | An optimization is already running for this plan. Wait for it to finish before issuing another. |
highway.optimization.error.no_result_found | The optimizer ran but couldn't produce a feasible solution. |
highway.optimization.error.too_much_requests | The optimizer backend rate-limited the request. |
Recovery patterns
Retry only on 429 and 5xx. Other 4xx codes will fail the same way on retry — fix the request first.
Use exponential backoff with jitter when retrying:
async function withRetry(fn, attempt = 0) {
try {
return await fn();
} catch (err) {
if (attempt >= 5 || !isRetryable(err)) throw err;
const baseMs = 500 * 2 ** attempt; // 500, 1000, 2000, 4000, 8000
const jitterMs = Math.random() * 500;
await new Promise((r) => setTimeout(r, baseMs + jitterMs));
return withRetry(fn, attempt + 1);
}
}
const isRetryable = (err) =>
err.status === 429 || (err.status >= 500 && err.status < 600);Log the message_id, not just the message. The message text may change for clarity; the message_id is stable across releases.
Reporting issues
If you hit a 5xx that persists, or a message_id you can't make sense of, email developers@routal.com and include:
- The endpoint (method + path)
- The full response body (
messageandmessage_id) - An approximate timestamp (UTC)
That's enough to trace the request server-side.
