> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solvice.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error body, every status and code, unassigned-job categories and reasons, and how to fix each

## 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](#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](#authentication-errors) and [429 Too Many Requests](#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`:

```json 400 Bad Request theme={null}
{
  "status": "BAD_REQUEST",
  "code": "invalid_request",
  "message": "2 problems in request",
  "request_id": "req_7f3a2c90d1e4b5a6",
  "errors": [
    {
      "code": "limit_exceeded",
      "pointer": "/options/time_limit_s",
      "message": "options.time_limit_s must be <= 300, got 600",
      "limit": 300,
      "actual": 600
    },
    {
      "code": "out_of_range",
      "pointer": "/problem/jobs/1/priority",
      "message": "job 'delivery-2': priority must be >= 1, got 0"
    }
  ]
}
```

<ResponseField name="status" type="string">
  The HTTP status as text: `BAD_REQUEST`, `FORBIDDEN`, `PAYLOAD_TOO_LARGE`, `UNSUPPORTED_MEDIA_TYPE`, `INTERNAL_SERVER_ERROR`, `BAD_GATEWAY` or `SERVICE_UNAVAILABLE`.
</ResponseField>

<ResponseField name="code" type="string">
  What kind of failure this is. One value per HTTP status; see the [table below](#status-codes). Branch on this.
</ResponseField>

<ResponseField name="message" type="string">
  A summary for people. With one item in `errors[]` it is that item's message; with several it is the count, as above.
</ResponseField>

<ResponseField name="request_id" type="string">
  Identifies this request. The same value is returned in the `x-request-id` response header. Quote it when you contact support.
</ResponseField>

<ResponseField name="errors" type="array">
  One entry per problem found in the request. Present on a `400`; absent on other statuses.
</ResponseField>

<ResponseField name="errors[].code" type="string">
  What is wrong with this field. See [Item codes](#item-codes).
</ResponseField>

<ResponseField name="errors[].pointer" type="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](#pointers).
</ResponseField>

<ResponseField name="errors[].message" type="string">
  A description for people. Do not parse it; branch on `code` and `pointer`.
</ResponseField>

<ResponseField name="errors[].limit" type="integer">
  On `limit_exceeded` only: the cap.
</ResponseField>

<ResponseField name="errors[].actual" type="integer">
  On `limit_exceeded` only: the count your request has.
</ResponseField>

### Pointers

A pointer is a path from the root of the body you sent. Array elements are addressed by index.

| Pointer | Points at |
| - | - |
| `/problem/jobs/1/priority` | `priority` of the second job |
| `/problem/vehicles/0/shifts/0/from` | `from` of the first shift of the first vehicle |
| `/problem/relations/2` | The third relation |
| `/plan/routes/0/stops/3` | The fourth stop of the first plan route |
| `/options/time_limit_s` | `time_limit_s` in `options` |

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.

| Field | Appears in | What it is |
| - | - | - |
| `errors[].pointer` | A `400` error body | A JSON Pointer from the root of the request body, so it names `problem`, `plan` or `options` first. It addresses array elements by index |
| `unassigned[].relaxations[].field` | A `200` from solve or evaluate | A label that names the task by id: `/jobs/<id>/demand` or `/jobs/<id>/time_windows`. It has no `/problem` prefix and uses `/jobs/` for a shipment too, so it does not resolve against the request body. Find the task through `job_id` |
| `suggestions[].reason` | A `200` from suggest | A sentence for people, present when a task has no options. See [When a task gets no options](/guides/vrp/v3/suggest#when-a-task-gets-no-options) |

### 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

| HTTP | `code` | Meaning | Retry? |
| - | - | - | - |
| `400` | `invalid_request` | The request is invalid. `errors[]` lists each problem | No. Fix the request |
| `401` | n/a | The API key is missing or not accepted. Sent by the gateway with a [different body](#authentication-errors) | No. Fix the key |
| `403` | `feature_not_enabled` | The request uses a feature your account does not have. Today that is `travel.engine: "tomtom_real_time"`. The key itself is fine | No. Remove the feature or contact Solvice |
| `413` | `payload_too_large` | The body is larger than 10 MiB after decompression | No. Send a smaller problem |
| `415` | `unsupported_media_type` | `Content-Type` is not `application/json`, or `Content-Encoding` is something other than `gzip` | No. Fix the headers |
| `429` | n/a | You are over the rate limit. Sent by the gateway, [not in the error body](#429-too-many-requests) | Yes, with backoff |
| `500` | `internal` | A fault on the server side | Once, then contact support with the `request_id` |
| `502` | `matrix_provider_error` | Travel times and distances could not be fetched | Yes, with backoff |
| `503` | `overloaded` | No capacity right now | Yes, after the number of seconds in the `Retry-After` header |

<Note>
  Branch on the HTTP status first, then on `code`. Read `code` only on a status that has one in this table.
</Note>

### 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.

```json 400: unknown field theme={null}
{
  "status": "BAD_REQUEST",
  "code": "invalid_request",
  "message": "unknown field `time_limit`, expected one of `time_limit_s`, `seed`, `polylines` at line 1 column 688",
  "request_id": "req_2b9d41c07a35e6f8",
  "errors": [
    {
      "code": "unknown_field",
      "pointer": "/options/time_limit",
      "message": "unknown field `time_limit`, expected one of `time_limit_s`, `seed`, `polylines` at line 1 column 688"
    }
  ]
}
```

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](/guides/vrp/v3/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](/guides/vrp/v3/concepts/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.

```json 503 Service Unavailable theme={null}
{
  "status": "SERVICE_UNAVAILABLE",
  "code": "overloaded",
  "message": "All solver slots are busy, try again later",
  "request_id": "req_91c4e02f6b7d3a58"
}
```

## 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.

| `errors[].code` | Meaning | Typical cause |
| - | - | - |
| `invalid_json` | The body is not valid JSON | Trailing comma, unquoted key, truncated body |
| `invalid_type` | A value has the wrong JSON type, or is not one of the allowed values | A string where a number is expected; an unknown relation `type`; a skill sent as `"fridge"` rather than `{ "name": "fridge" }` |
| `unknown_field` | A field the schema does not define | A typo, a camelCase name, a V2 field |
| `missing_field` | A required field is absent | A job, vehicle or shift without `id`; a shift without `from` or `to` |
| `invalid_value` | A value is well-formed but not acceptable | A shift id that is empty or too long; a misplaced `reload` stop in `plan` |
| `not_supported` | A field or value that exists in the schema but is not available yet | `break_interruptible: false`; more than one of `fixed`, `per_route_hour`, `per_stop` on a vehicle's `cost` |
| `out_of_range` | A number outside its allowed range | A negative `drop_fee`; `priority: 0`; a negative `max_overtime_s` |
| `duplicate_id` | An id used twice where ids must be unique | Two vehicles with the same `id`; a stop listed twice in `plan` |
| `unknown_reference` | A reference to an id that does not exist | A depot, vehicle, job or shift id that is not in `problem` |
| `conflicting_fields` | Two fields that cannot be combined, or one that needs another | `drop_fee` with `priority`; `mandatory: false` without `drop_fee` |
| `limit_exceeded` | A count over a published limit. Carries `limit` and `actual` | More than 10,000 tasks; `time_limit_s` above 300. See [Limits](/guides/vrp/v3/limits) |

## 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:

<CodeGroup>
  ```json 401: key not accepted theme={null}
  {
    "message": "Invalid authentication token",
    "error": "GATEWAY_ERROR"
  }
  ```

  ```text 401: no Authorization header theme={null}
  (empty body)
  ```
</CodeGroup>

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](/guides/platform/authentication).

## Unassigned jobs

A `200` from `/v3/routing/solve` does not mean every job was planned. Check `summary.status`:

| `summary.status` | Meaning |
| - | - |
| `solved` | Every job and shipment is on a route. `unassigned[]` is empty |
| `partial` | Some are planned and some are not |
| `infeasible` | None is planned |

<Warning>
  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`.
</Warning>

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:

```json theme={null}
{
  "unassigned": [
    {
      "job_id": "delivery-7",
      "category": "blocked",
      "reasons": [
        {
          "code": "TIME_WINDOW_VIOLATED",
          "message": "The job's time window cannot be satisfied by any route"
        }
      ],
      "relaxations": [
        {
          "field": "/jobs/delivery-7/time_windows",
          "op": "extend_time_window",
          "amount": 900,
          "unit": "s",
          "suggestion": "Extend a time window by 900s (~15 min)."
        }
      ]
    }
  ]
}
```

<ResponseField name="unassigned[].job_id" type="string">
  The job id, or the shipment id for a shipment. A shipment is listed once, not once per stop.
</ResponseField>

<ResponseField name="unassigned[].category" type="string">
  What kind of fix can work. One of the four values in [Categories](#categories). Always present.
</ResponseField>

<ResponseField name="unassigned[].reasons" type="array">
  The rule that kept the task out, as a `code` from [Reason codes](#reason-codes) and a `message` for people.
</ResponseField>

<ResponseField name="unassigned[].relaxations" type="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.
</ResponseField>

Each relaxation has an `op`, an `amount` in `unit`, a `suggestion` sentence and a `field`:

| `op` | `unit` | Meaning |
| - | - | - |
| `reduce_demand` | `load` | Lower the task's demand by `amount`, or add that much capacity to a vehicle that can serve it |
| `extend_time_window` | `s` | Widen a time window by `amount` seconds |

`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](#pointers-fields-and-reasons).

### Categories

Start with `category`. It tells you what kind of fix can work.

| `category` | Meaning | What to do |
| - | - | - |
| `impossible` | No vehicle can ever serve this job, whatever the other jobs do | Fix the input: skills, tags, eligibility, a locked vehicle, a time window no shift overlaps, or a load no vehicle can carry |
| `blocked` | Some vehicle could serve it alone, but not alongside the jobs that are planned | Add capacity or working time, widen windows, or accept that it does not fit. More solve time alone is unlikely to help |
| `priced_out` | The job has a `drop_fee`, a slot for it exists, and serving it there costs at least the fee | Working as designed. Raise the `drop_fee` if it should be served. More solve time does not help |
| `search_limit` | A feasible slot exists and the search did not use it in its time budget | Raise `options.time_limit_s` |

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`.

| `reasons[].code` | Comes with `category` | Meaning | What to check |
| - | - | - | - |
| `CAPACITY_EXCEEDED` | `impossible` or `blocked` | No vehicle has enough capacity, or the ones that do are full | `demand` and `delivery` against `vehicles[].capacity`; dimension names must match exactly on both sides |
| `TIME_WINDOW_VIOLATED` | `impossible` or `blocked` | The time window cannot be met by any route. With `impossible`, no shift overlaps the window at all | `time_windows` against shift hours and travel time; dates and UTC offsets |
| `CAPACITY_AND_TIME_WINDOW` | `impossible` or `blocked` | Capacity rules out some vehicles and time windows the others | Both of the above |
| `SKILLS_MISMATCH` | `impossible` | No vehicle provides the required skills | `skills[].name` spelling and `min_level` on job and vehicle |
| `TAG_RESTRICTED` | `impossible` | No shift serves any of the job's tags | `jobs[].tags` against `shifts[].serves_tags` |
| `COMMITMENT_BLOCKED` | `impossible` | The job is pinned to a vehicle that cannot serve it | `locked_vehicle` |
| `VEHICLE_EXCLUDED` | `impossible` | The allowed and excluded lists rule out every vehicle | `eligible_vehicles` |
| `VEHICLE_RANGE_EXCEEDED` | `blocked` | Serving it would exceed a distance or duration limit | `vehicles[].limits` |
| `UNREACHABLE_LOCATION` | `impossible` or `blocked` | There is no road connection to or from the location | The coordinate: `[longitude, latitude]` order, on land, near a road |
| `SOLVER_LIMIT` | `search_limit` or `priced_out` | A feasible slot exists and the job was not put in it | Read `category`. `search_limit`: raise `options.time_limit_s`. `priced_out`: the `drop_fee` is cheaper than serving the job, so more time does not help |
| `ALL_ROUTES_BLOCKED` | `impossible` or `blocked` | Different vehicles are ruled out for different reasons, or the rule has no code of its own | Each rule on the job against each vehicle that could serve it |

`SOLVER_LIMIT` is the one code that covers two categories, so branch on `category` and not on the code. The [Constraints](/guides/vrp/v3/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](/guides/vrp/v3/evaluate#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:

| `category` on evaluate | Meaning |
| - | - |
| `impossible` | No vehicle can serve the task at all |
| `blocked` | Some vehicle could serve it alone, but it does not fit into the plan you sent |
| `priced_out` | It fits into the plan, it has a `drop_fee`, and serving it at the cheapest position costs at least the fee |
| `search_limit` | It fits into the plan you sent. It is unassigned only because your plan does not include it |

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`](/guides/vrp/v3/suggest) where it fits best.

## Troubleshooting

<AccordionGroup>
  <Accordion title="I get a 400 for a field that used to work in V2">
    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](/guides/vrp/v3/migration-from-v2) for the mapping.
  </Accordion>

  <Accordion title="Every job comes back unassigned">
    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.
  </Accordion>

  <Accordion title="A job is unassigned with search_limit">
    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](#on-evaluate).
  </Accordion>

  <Accordion title="Suggest returns no options for a task">
    The entry carries a `reason`, and the top-level `truncated` flag says whether the answer is final. See [When a task gets no options](/guides/vrp/v3/suggest#when-a-task-gets-no-options).
  </Accordion>

  <Accordion title="Solve rejects my warm-start plan with a 400">
    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`](/guides/vrp/v3/evaluate): it scores the plan as it is and lists each rule it breaks, stop by stop.
  </Accordion>

  <Accordion title="I get a 415 although I send JSON">
    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`.
  </Accordion>

  <Accordion title="I want to check requests in CI without being billed">
    Add `?validate_only=true`. The request goes through the same validation as a real call and returns before any travel-time fetch or search.
  </Accordion>
</AccordionGroup>

<Columns cols={2}>
  <Card title="Limits" icon="gauge" href="/guides/vrp/v3/limits">
    Request limits and what you get when you exceed one.
  </Card>

  <Card title="Authentication" icon="key" href="/guides/platform/authentication">
    API keys, the `401` response and rate limiting.
  </Card>

  <Card title="Evaluate" icon="scale-balanced" href="/guides/vrp/v3/evaluate">
    See exactly which rules a plan breaks.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/vrp/v3/introduction">
    Request and response schema for every endpoint.
  </Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.