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

# Evaluate

> Score a plan you already have: timeline, cost and constraint violations, without optimising

## Overview

`POST /v3/routing/evaluate` takes a problem and a plan and tells you what that plan is worth. It computes arrival times, loads and the cost breakdown exactly as a solve would, and lists every hard constraint the plan breaks. It never moves a stop.

Typical uses:

* Check a plan a dispatcher edited by hand before you commit it.
* Compare your current planning against a solve result on the same cost model.
* Get the full timeline after you apply a [suggestion](/guides/vrp/v3/suggest).
* Re-score yesterday's plan against today's constraints.

## The request

<ParamField body="problem" type="object" required>
  The same `problem` object as on `/v3/routing/solve`. See [the V3 API model](/guides/vrp/v3/api-design).
</ParamField>

<ParamField body="plan" type="object" required>
  The plan to score: `routes[]`, one entry per vehicle shift that serves something.
</ParamField>

Evaluate takes no `options`: there is no search, so there is nothing to tune. An `options` key is rejected as an unknown field.

### The plan shape

The value of `plan`:

```json theme={null}
{
  "routes": [
    {
      "vehicle": "truck-1",
      "shift": "mon",
      "stops": [
        { "type": "job", "id": "delivery-2" },
        { "type": "job", "id": "delivery-1" }
      ]
    }
  ]
}
```

| Field | Meaning |
| - | - |
| `routes[].vehicle` | A `vehicles[].id` from the problem |
| `routes[].shift` | The id of one of that vehicle's `shifts[]`. Each vehicle shift may appear once |
| `routes[].stops[]` | The task stops in visiting order |
| `routes[].locked_count` | Optional. Used by solve and suggest; evaluate only checks that it does not exceed the number of stops and is not combined with a `reload` stop |

A stop is one of:

| Stop | Meaning |
| - | - |
| `{ "type": "job", "id": "..." }` | A job, by its id |
| `{ "type": "pickup", "id": "..." }` | A shipment's pickup. The id is the pickup's own `id` if you gave it one, otherwise `<shipment id>:pickup` |
| `{ "type": "delivery", "id": "..." }` | A shipment's delivery, named the same way with `:delivery`. It must come after its pickup on the same route |
| `{ "type": "reload", "depot": "..." }` | A mid-route return to a depot listed in that shift's `reloads`. It starts a new trip and must sit between two task stops. One route can reload at the same depot at most 8 times |

Start, end and break stops are not listed; they are placed for you. Every solve and evaluate response returns its routes in this shape as `plan`, so a result's `plan` can be sent back unchanged.

A task you leave out of every route is not an error. It is reported in `unassigned[]`, with a category that says whether it could be added: see [What unassigned means on evaluate](#what-unassigned-means-on-evaluate).

### Example

The quickstart's two deliveries, with `delivery-1` due by 11:00 local time and a hand-made plan that visits `delivery-2` first:

```bash theme={null}
curl -X POST https://api.solvice.io/v3/routing/evaluate \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "problem": {
      "jobs": [
        {
          "id": "delivery-1",
          "location": { "coordinate": [4.7005, 50.8798] },
          "service_duration_s": 300,
          "time_windows": [
            { "from": "2026-04-01T09:00:00+02:00", "to": "2026-04-01T11:00:00+02:00" }
          ],
          "delivery": { "weight": 10 }
        },
        {
          "id": "delivery-2",
          "location": { "coordinate": [3.7303, 51.0500] },
          "service_duration_s": 600,
          "time_windows": [
            { "from": "2026-04-01T10:00:00+02:00", "to": "2026-04-01T15:00:00+02:00" }
          ],
          "delivery": { "weight": 20 }
        }
      ],
      "vehicles": [
        {
          "id": "truck-1",
          "capacity": { "weight": 100 },
          "shifts": [
            {
              "id": "mon",
              "from": "2026-04-01T08:00:00+02:00",
              "to":   "2026-04-01T17:00:00+02:00",
              "start": { "location": { "coordinate": [4.3517, 50.8503] } },
              "end":   { "location": { "coordinate": [4.3517, 50.8503] } }
            }
          ]
        }
      ]
    },
    "plan": {
      "routes": [
        {
          "vehicle": "truck-1",
          "shift": "mon",
          "stops": [
            { "type": "job", "id": "delivery-2" },
            { "type": "job", "id": "delivery-1" }
          ]
        }
      ]
    }
  }'
```

## The response

The response has the same four parts as a solve response: `summary`, `routes`, `plan` and `unassigned`. The figures below are illustrative; yours depend on live travel times.

```json theme={null}
{
  "summary": {
    "status": "infeasible",
    "vehicles_used": 1,
    "jobs_assigned": 2,
    "jobs_unassigned": 0,
    "total_distance_m": 187592,
    "total_duration_s": 9733,
    "score": {
      "violations": 1,
      "unserved_priority": 0,
      "cost": 178.76944,
      "components": {"fixed": 0.0, "time": 141.25104, "distance": 37.5184, "waiting": 0.0, "overtime": 0.0, "lateness": 0.0, "window_choice": 0.0, "target": 0.0, "tag_preference": 0.0, "depot_visits": 0.0, "grouping": 0.0, "drop_fees": 0.0, "other": 0.0}
    },
    "elapsed_ms": 121
  },
  "routes": [
    {
      "vehicle": "truck-1",
      "shift": "mon",
      "distance_m": 187592,
      "duration_s": 9733,
      "shift_duration_s": 14532,
      "overtime_s": 0,
      "load_peak": {"weight": 30},
      "stops": [
        {
          "type": "start",
          "location": [4.3517, 50.8503],
          "arrival": "2026-04-01T06:00:00Z",
          "departure": "2026-04-01T06:00:00Z",
          "service_s": 0,
          "load_after": {"weight": 30}
        },
        {
          "type": "job",
          "id": "delivery-2",
          "location": [3.7303, 51.05],
          "arrival": "2026-04-01T06:55:01Z",
          "departure": "2026-04-01T08:10:00Z",
          "wait_s": 3899,
          "service_s": 600,
          "travel_time_s": 3301,
          "travel_distance_m": 63610,
          "lateness_s": 0,
          "load_after": {"weight": 10}
        },
        {
          "type": "job",
          "id": "delivery-1",
          "location": [4.7005, 50.8798],
          "arrival": "2026-04-01T09:29:20Z",
          "departure": "2026-04-01T09:34:20Z",
          "wait_s": 0,
          "service_s": 300,
          "travel_time_s": 4760,
          "travel_distance_m": 91802,
          "lateness_s": 1760,
          "load_after": {"weight": 0},
          "violations": [
            {
              "type": "time_window",
              "message": "Arrives 1760s after latest allowed time"
            }
          ]
        },
        {
          "type": "end",
          "location": [4.3517, 50.8503],
          "arrival": "2026-04-01T10:02:12Z",
          "departure": "2026-04-01T10:02:12Z",
          "service_s": 0,
          "travel_time_s": 1672,
          "travel_distance_m": 32180,
          "load_after": {"weight": 0}
        }
      ]
    }
  ],
  "plan": {
    "routes": [
      {
        "vehicle": "truck-1",
        "shift": "mon",
        "stops": [
          { "type": "job", "id": "delivery-2" },
          { "type": "job", "id": "delivery-1" }
        ]
      }
    ]
  },
  "unassigned": []
}
```

The truck reaches `delivery-1` at 09:29:20 UTC, 1,760 seconds after its window closed at 09:00:00 UTC. The plan is still scored in full, so you can see what it would cost and how late it would be.

### How it differs from a solve response

| Field | On evaluate |
| - | - |
| `summary.status` | `feasible` when the plan breaks no hard constraint, `infeasible` when it breaks at least one. It says nothing about tasks left out of the plan |
| `summary.score.violations` | The number of violations across all routes and stops. On a solve response it is always `0` |
| `routes[].stops[].violations` | Violations found at that stop. Absent when there are none |
| `routes[].violations` | Violations that belong to the route as a whole. Absent when there are none |
| `summary.iterations` | Not present: nothing is searched |
| `unassigned[]` | Every task of `problem` that is in no route of your plan, in the same shape as on solve. The categories read differently: see below |

Everything else, including the stop timeline and `score.components`, reads exactly as on solve. See the [Quickstart](/guides/vrp/v3/quickstart) for the stop fields and [Objective function](/guides/vrp/v3/concepts/scoring-explanation) for the cost breakdown.

In a request that contains shipments, `summary.jobs_assigned` counts a shipment in the plan twice, once for each of its stops, and a shipment left out of the plan once. Use the length of `unassigned[]`, which has one entry per job or shipment, to count what the plan leaves out.

### What unassigned means on evaluate

Evaluate never adds a task to your plan. For each task the plan leaves out, `unassigned[].category` tells you whether it could be added to the plan as it stands:

| `category` | On evaluate |
| - | - |
| `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, it has a `drop_fee`, and serving it at the cheapest position costs at least the fee |
| `search_limit` | It fits. It is unassigned only because your plan does not include it |

A task that fits is reported with reason code `SOLVER_LIMIT` and a `message` about a time budget. That wording comes from solve; evaluate has no time budget. To place such a task, add it to `plan` or ask [`/v3/routing/suggest`](/guides/vrp/v3/suggest) for its best position. The reason codes are listed in [Errors](/guides/vrp/v3/errors#reason-codes).

## Violations

Each violation is `{ "type", "message" }`. `type` is a stable string to branch on; `message` is for people.

| `type` | Reported on | The plan breaks |
| - | - | - |
| `time_window` | stop | The task's time window: the vehicle arrives after it closes |
| `capacity` | stop or route | The vehicle's `capacity` in some dimension |
| `skills` | stop | The task's required `skills` |
| `tags` | stop | The shift's `serves_tags` |
| `eligibility` | stop | The task's `eligible_vehicles` |
| `commitment` | stop | The task's `locked_vehicle` |
| `first_job` | stop | A task that must be the first stop of its route is not. A task gets this rule from an `ordered` [relation](/guides/vrp/v3/constraints#relations) that lists one job only |
| `unreachable` | stop or route | There is no road connection for a leg of the route |
| `ordered` | stop | An `ordered` relation |
| `shipment` | stop | Pickup and delivery pairing |
| `same_resource` | stop | A `same_resource` relation |
| `same_day` | stop | A `same_day` relation |
| `synchronized` | stop | A `synchronized` relation |
| `time_lag` | stop | A `time_lag` relation |
| `shift_end` | route | The route finishes after the shift's end plus allowed overtime |
| `max_distance` | route | `limits.max_distance_m` |
| `max_duration` | route | `limits.max_route_duration_s` |
| `max_tasks` | route | `shifts[].max_tasks` |
| `break` | route | A break in `shifts[].breaks` cannot be taken as required |
| `multi_trip` | route | The shift's reload rules |
| `period_limit` | route | A `limits.periods[]` cap. Reported once per period, on the route of the first shift used in it |
| `quota` | route | A `quotas[]` cap. Reported once per quota window, the same way |
| `infeasible` | stop | A stop the solver would refuse, with no more specific type. Not expected in practice |

New types can be added as new constraints ship. Handle the ones you care about and treat anything else as a generic violation.

<Note>
  A time window priced as soft, through `time_windows[].per_late_hour` or `objective.costs.per_late_hour`, is not a violation when it is missed. The lateness shows up in the stop's `lateness_s` and in `score.components.lateness` instead.
</Note>

## What evaluate checks, and when

Evaluate needs real travel times to compute a timeline, so it fetches them the same way solve does. What it skips is the search.

1. **The request is validated.** Schema, references and [limits](/guides/vrp/v3/limits) are checked first. A problem here is a `400` and no travel times are fetched.
2. **Travel times are fetched.**
3. **The plan is matched to the problem.** A plan that cannot be laid out at all is a `400` with pointer `/plan/routes/{i}` or `/plan/routes/{i}/stops/{k}`: an unknown vehicle, shift, stop or depot id; a route or stop listed twice; a stop whose `type` does not match the task; a shipment whose pickup and delivery are not both on one route, pickup first; a misplaced `reload`; more than 8 reloads at one depot; more trips than `max_trips`; a `locked_count` larger than the number of stops.
4. **The plan is scored.** Everything else, such as a missed window or an overloaded vehicle, is reported as a violation in a `200` response.

### `validate_only`

`POST /v3/routing/evaluate?validate_only=true` runs step 1 only and returns `200 {"valid": true, "warnings": []}` or the `400` the real call would return. It checks `problem`; it does not look at `plan`, because matching the plan needs the later steps.

## Errors

Evaluate returns the same [error body](/guides/vrp/v3/errors) and status codes as solve. A plan with violations is not an error: it is a `200` with `summary.status: "infeasible"`.

<Columns cols={2}>
  <Card title="Suggest" icon="lightbulb" href="/guides/vrp/v3/suggest">
    Find the best slot for a new task in a plan you keep.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/guides/vrp/v3/errors">
    The error body, every code, and what to do about each.
  </Card>

  <Card title="Constraints" icon="cog" href="/guides/vrp/v3/constraints">
    The rules behind each violation type.
  </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.