Skip to main content

Overview

There are two kinds of “it didn’t work” on the V3 Routing API, and they look different:
  • The request failed. You get a 4xx or 5xx status and an error body. Nothing was planned.
  • The request succeeded but not every job was planned. You get a 200. summary.status is partial or infeasible, and unassigned[] says which jobs were left out and why. See Unassigned jobs.
Got a 401 or a 429? Those come from the API gateway, before the request reaches the routing service, and do not use the error body below. See Authentication errors and 429 Too Many Requests.

The error body

Every error the routing service returns from /v3/routing/solve, /v3/routing/evaluate and /v3/routing/suggest has this JSON body. That covers the statuses 400, 403, 413, 415, 500, 502 and 503:
400 Bad Request
string
The HTTP status as text: BAD_REQUEST, FORBIDDEN, PAYLOAD_TOO_LARGE, UNSUPPORTED_MEDIA_TYPE, INTERNAL_SERVER_ERROR, BAD_GATEWAY or SERVICE_UNAVAILABLE.
string
What kind of failure this is. One value per HTTP status; see the table below. Branch on this.
string
A summary for people. With one item in errors[] it is that item’s message; with several it is the count, as above.
string
Identifies this request. The same value is returned in the x-request-id response header. Quote it when you contact support.
array
One entry per problem found in the request. Present on a 400; absent on other statuses.
string
What is wrong with this field. See Item codes.
string
A JSON Pointer (RFC 6901) to the offending place in the request body. Absent when the problem has no location, such as a body that is not JSON. See Pointers.
string
A description for people. Do not parse it; branch on code and pointer.
integer
On limit_exceeded only: the cap.
integer
On limit_exceeded only: the count your request has.

Pointers

A pointer is a path from the root of the body you sent. Array elements are addressed by index. For an unknown field the pointer usually ends at the unknown key. Inside a job, a shipment or a break it can stop at the enclosing object, such as /problem/jobs/0; the message always names the field. For a missing field the pointer names the object that lacks the field, and the message names the field. A problem that is only found after travel times are fetched can carry a less precise pointer: it can name a whole collection, or be absent. One case points at a key that does not exist in the request: when the coordinates span more than one region, the pointer is /problem/locations. The coordinates to check are the ones on your jobs, shipments and depots, and on the start and end of each shift.

Pointers, fields and reasons

Three response fields tell you where to look. Only the first is a JSON Pointer.

How many problems are reported

  • A body that cannot be read at all (bad JSON, unknown field, missing field, wrong type) reports the first problem only. Fix it and resend.
  • Once the body reads cleanly, validation reports every problem it finds in one response, up to 100 items, so you can fix them in one pass.

Status codes

Branch on the HTTP status first, then on code. Read code only on a status that has one in this table.

400 Bad Request

Unknown fields are a 400, never ignored. So are a misspelled field, a wrong type and a field the API does not support yet. A request that returns 200 was read exactly as you sent it.
400: unknown field
To check a request without running it, add ?validate_only=true. You get 200 {"valid": true, "warnings": []} or the same 400 the real call would return, with no travel-time fetch and no search. It validates problem and options; a plan is checked against the problem only by the real call. The parameter takes the literal true or false. Any other value, such as validate_only=1, is a 400 with a plain-text body, not the error body above.

413 and 415

The 10 MiB limit applies to the body after gzip decompression. To send a compressed body, set Content-Encoding: gzip and keep Content-Type: application/json. See Limits.

502 Bad Gateway

The solver could not get travel times for your locations from the routing data provider. The request itself may be fine. Retry with backoff; if it keeps failing for the same request, contact support with the request_id. Coordinates that span more than one region or continent are a different case: that is a 400 with item code invalid_value, and usually means a coordinate is in [latitude, longitude] order. V3 coordinates are [longitude, latitude]. See Distance matrices for how travel data is fetched.

429 Too Many Requests

Requests are rate limited by the API gateway. A request over the limit is rejected with 429 before it reaches the routing service, so its body is not the error body on this page: detect it from the HTTP status and do not read code or request_id from it. Back off and retry.

503 Service Unavailable

The service is at capacity, or travel-time lookups are temporarily suspended after repeated failures. Every 503 carries a Retry-After header in seconds. Wait that long and retry; add backoff if it repeats.
503 Service Unavailable

Item codes

errors[].code on a 400. New values can be added, so handle a value you do not recognise as a generic invalid request.

Authentication errors

A request with a missing or unrecognised key is rejected by the API gateway with 401 before it reaches the solver. The body is not the error body above, and comes in two shapes:
Detect it from the HTTP status, and do not parse the body as JSON without checking it is non-empty. The header is Authorization: YOUR_API_KEY, with no Bearer prefix. See Authentication.

Unassigned jobs

A 200 from /v3/routing/solve does not mean every job was planned. Check summary.status:
In a request that contains shipments, summary.jobs_assigned does not count tasks. A planned shipment adds 2 to it, for its pickup and its delivery, and an unplanned shipment still adds 1. For such a request status is partial, not infeasible, when nothing is planned.solved and unassigned[] are exact for every request. To count what was left out, use the length of unassigned[]: it has one entry per job or shipment. To detect that nothing was planned, check that summary.vehicles_used is 0.
The plan the solver returns never breaks a hard constraint. A job that cannot be served within the rules is left out and reported, with a reason:
string
The job id, or the shipment id for a shipment. A shipment is listed once, not once per stop.
string
What kind of fix can work. One of the four values in Categories. Always present.
array
The rule that kept the task out, as a code from Reason codes and a message for people.
array
A best-effort hint at the smallest change that would let the task fit. Empty when no simple change exists, such as for a skills mismatch.
Each relaxation has an op, an amount in unit, a suggestion sentence and a field: field names the task by id, as in /jobs/delivery-7/time_windows, not by its index in the request. It uses /jobs/ for a shipment as well. See Pointers, fields and reasons.

Categories

Start with category. It tells you what kind of fix can work. One case of impossible depends on the plan. A task in a same_resource or same_day relation whose partner is already planned can only go on that vehicle or that day. When no shift there can serve it, the task is impossible, although another vehicle or day could take it if the partner moved.

Reason codes

reasons[].code is one of eleven values. Treat a value you do not recognise as ALL_ROUTES_BLOCKED. SOLVER_LIMIT is the one code that covers two categories, so branch on category and not on the code. The Constraints page lists which rule produces which code.

On evaluate

/v3/routing/evaluate does not search, so unassigned[] and summary.status mean something different there:
  • summary.status is feasible or infeasible and describes the rules your plan breaks. It says nothing about tasks the plan leaves out. Broken rules are reported as violations.
  • unassigned[] lists every task that is in no route of your plan, in the same shape as above. The category then describes whether the task could be added to the plan as it stands:
A task that fits comes back with code SOLVER_LIMIT and a message that mentions a time budget. Evaluate has no time budget and takes no options, so there is nothing to raise: add the task to plan, or ask /v3/routing/suggest where it fits best.

Troubleshooting

V3 uses snake_case names and a different structure, and rejects every field it does not define. The pointer shows which field. See Migration from V2 for the mapping.
Look at category and reasons[].code on a few of them. TIME_WINDOW_VIOLATED with category impossible on all of them usually means the job windows and the shifts are on different dates or in different time zones. A timestamp without a UTC offset is accepted and read as UTC, not as local time, so send an offset or Z on every timestamp. UNREACHABLE_LOCATION on all of them usually means coordinates are in [latitude, longitude] order.
On solve, the default options.time_limit_s is 1 second. Larger problems need more: raise it, up to 300. On evaluate, search_limit means the task fits but is not in your plan: see On evaluate.
The entry carries a reason, and the top-level truncated flag says whether the answer is final. See When a task gets no options.
A stop inside a route’s locked_count cannot be moved or dropped, so if it breaks a hard rule the request fails. Send the same problem and plan to /v3/routing/evaluate: it scores the plan as it is and lists each rule it breaks, stop by stop.
The Content-Type header must be application/json. Some HTTP clients send text/plain or a form type unless told otherwise. If you compress the body, the only accepted Content-Encoding is gzip.
Add ?validate_only=true. The request goes through the same validation as a real call and returns before any travel-time fetch or search.

Limits

Request limits and what you get when you exceed one.

Authentication

API keys, the 401 response and rate limiting.

Evaluate

See exactly which rules a plan breaks.

API reference

Request and response schema for every endpoint.