Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.

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.tasks.unassigned is above 0, 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 or a query parameter, which the message names. 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. For options.time_limit_s it is the value you sent, rounded up to whole seconds.

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, a break or a relation 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. An unknown query parameter has no pointer; the message names it. 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. The first two are JSON Pointers.

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 the problems it finds together, up to 100 items. It may list only the first problem of each job, shipment, vehicle or depot, and it checks what ties entries together (vehicle references, relations, quotas, the objective) only once every entry is valid on its own. A corrected request can therefore show new problems: fix and resend until it passes.
  • A problem with the plan, which is matched to the problem only after travel times are fetched, is reported on its own, one at a time.

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, send it to POST /v3/routing/validate/solve, /v3/routing/validate/evaluate or /v3/routing/validate/suggest, the one that matches the endpoint. You get 200 {"valid": true, "warnings": []} or the same 400 the real call would return (or its 403, for a feature your account does not have), with no travel-time fetch and no search. It validates problem and options. Coordinates that span more than one region and the plan (its references and whether it imports, warm-start plan on solve included) are checked only by the real call, after the travel-time fetch; suggest’s own check that plan leaves a task to place does run here. The body is checked as the chosen endpoint checks its own, so a field that endpoint does not take, such as options on evaluate, is the same unknown_field 400. The validate endpoints take no query parameter, and neither do solve, evaluate and suggest. Any query parameter, including a misspelt name or the removed validate_only, is a 400 with the error body above and an item of code unknown_field; query items carry no pointer, the parameter is named in message.

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. Only a solve or a suggest returns this error, and only with options.travel_fallback: "fail". By default, travel times that cannot be fetched, for any reason, are estimated: /v3/routing/solve plans on the estimate and summary.travel_estimated says so; /v3/routing/suggest ranks on it and sets travel_estimated. options.travel_fallback takes "estimate" (the default) or "fail", which returns the 502 or 503 instead. /v3/routing/evaluate has no such option and always estimates, setting summary.travel_estimated. See Distance matrices. 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. See Authentication. POST /v3/routing/runs also answers 429, with Retry-After: 30, while 10 runs of your account are queued or running: see Async runs.

503 Service Unavailable

The service is at capacity, or, on a solve or suggest with travel_fallback: "fail", travel-time lookups are temporarily suspended after repeated failures. Every 503 carries a Retry-After header, always 1 second. It is a floor, not a forecast: wait that long and retry, and 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.tasks: A job is one task and a shipment is one task. Every task is on a route when unassigned is 0; none is when served is 0. This section is the reference for each category and reason code. For the order to read them in, how to confirm a diagnosis with /v3/routing/evaluate and /v3/routing/suggest, and the ranked fixes, see Handling infeasibility. 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
Never empty. Each reason names the constraint that kept the task out on at least one vehicle and carries a message for people. A task lists each constraint once. For priced_out and time_limit no rule did, so there is one reason without a constraint. See Reasons.
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 message sentence and a pointer: pointer is a JSON Pointer into the request body, as in /problem/jobs/6/time_windows for the seventh job. A shipment is under /problem/shipments/, as in /problem/shipments/0/load. 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_vehicle 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.

Reasons

Each reason names one constraint that refused the task, in the same vocabulary as evaluate’s violation codes, and carries a message for people. There is no separate outcome code: category says what kind of fix can work, and constraint says which rule refused the task. A priced_out or time_limit task has one reason with no constraint. A task lists each constraint once, unreachable first, so a task refused for capacity on one vehicle and for its time window on another has two reasons, capacity and time_window. Treat a constraint you do not recognise as unknown. The Constraints page lists which rule produces which constraint.

On evaluate

/v3/routing/evaluate does not search, so unassigned[] and the score mean something different there:
  • summary.score.violation_count counts the rules your plan breaks; 0 means none. 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 one reason and no constraint, unless its all_or_none group holds it back (all_or_none) or its all-or-none crew cannot be placed together (synchronized, time_lag). 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[].constraint on a few of them. time_window with category impossible on all of them usually means the job windows and the shifts are on different dates or in different time zones. unreachable on all of them usually means coordinates are in [latitude, longitude] order.
On solve, the default options.time_limit_s is 150 ms plus 4 ms per task. Larger problems may need more: raise it, up to 60. On evaluate, time_limit means the task fits but is not in your plan: see On evaluate.
The entry carries a reason whose code says why: only infeasible is final, while time_limit and candidates_exhausted can change with a larger options.time_limit_s or options.max_insertions. For a crew, candidates_exhausted does not change with either; /v3/routing/solve may still place it. See When a task gets no insertions.
A stop inside a route’s locked_count cannot be dropped, so if it breaks a hard rule the request fails. errors[] lists each such stop (up to 100), each with the pointer of its stop (/plan/routes/{i}/stops/{k}) and the rule in the request’s terms, such as time window or shift end, capacity or eligible_vehicles; /v3/routing/validate/solve returns 200 for those cases. On /v3/routing/runs the run ends failed instead. A stop marked pinned: true outside a locked_count prefix (a shipment leg whose other leg is inside counts as inside) never fails the request: one that cannot be served on its vehicle comes back in unassigned[] with the rule that refuses it. 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.
Send it to POST /v3/routing/validate/solve, /v3/routing/validate/evaluate or /v3/routing/validate/suggest. 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.