Skip to main content
Routal

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:

FieldTypeRequiredDescription
messagestringyesError message
message_idstringyesError id

Example body:

{
  "message": "Domain not found",
  "message_id": "highway.domain.error.not_found"
}
  • message is human-readable, suitable for log lines but not for end-user display. Wording can change for clarity across releases.
  • message_id is 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

StatusMeaningWhat to do
200 OKRequest succeeded.Process the response body.
201 CreatedResource created.Read the new resource's id from the response body.
204 No ContentSuccess with no body (typical for DELETE).Treat as success.
400 Bad RequestPayload 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 UnauthorizedMissing or invalid private_key.Verify the key. See Authentication.
403 ForbiddenAuthenticated, but the credential does not have permission for this resource or action.Check project membership and the key's organization.
404 Not FoundResource ID does not exist (or has been deleted).Confirm the ID. Do not retry.
409 ConflictThe request collides with current resource state (e.g. deleting a locked route).Read current state, then decide.
429 Too Many RequestsYou've hit a rate limit.Back off and retry.
5xx Server ErrorSomething 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_idMeaning
highway.apiKey.error.not_foundThe 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_idMeaning
highway.plan.error.not_foundNo plan with the given id.
highway.route.error.not_foundNo route with the given id.
highway.stop.error.not_foundNo stop with the given stop_id.
highway.task.error.not_foundNo task with the given task_id.
highway.vehicle.error.not_foundNo vehicle with the given vehicle_id.

Validation

message_idMeaning
highway.validation.error.invalid_idA required ID field was not a valid 24-character hex string.
highway.stop.error.custom_fields_invalidA stop's custom_fields did not match the project's custom-field definitions.
highway.route.error.custom_fields_invalidSame, for a route.
highway.vehicle.error.custom_fields_invalidSame, for a vehicle.
highway.geocoding.error.wrong_lat_lngA 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_idMeaning
highway.plan.error.user_not_allowedAuthenticated, but the user/key lacks permission on the target plan (or its project).
highway.organization.error.user_not_allowedThe user is not part of the target organization.
highway.user.error.user_not_allowedThe action is not permitted for this user.

Resource state

message_idMeaning
highway.route.error.lockedCannot delete or modify a locked route (is_locked: true).
highway.route.error.not_in_transitThe action requires the route to be in_transit.
highway.stop.error.move_not_pending_stopsCannot move stops that are no longer pending.

Optimization

message_idMeaning
highway.optimization.error.sync_optimization_already_progressAn optimization is already running for this plan. Wait for it to finish before issuing another.
highway.optimization.error.no_result_foundThe optimizer ran but couldn't produce a feasible solution.
highway.optimization.error.too_much_requestsThe 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 (message and message_id)
  • An approximate timestamp (UTC)

That's enough to trace the request server-side.