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

# Suggest

> Rank the places a new job or shipment can go in an existing plan, without re-optimising it

## Overview

`POST /v3/routing/suggest` answers one question: *given the plan I already have, where can this new task go, and what does each option cost?* You send the problem, the plan you have committed to, and the ids of the tasks to place. For each task you get back a ranked list of feasible insertion positions, best first.

Use it when a new order arrives during the day and a dispatcher (or your own code) should pick a slot, rather than letting the solver rearrange routes that are already on the road. The committed plan is never changed: the API holds no state, so nothing happens until you apply an option yourself.

| You want to | Use |
| - | - |
| Build or rebuild routes | [`/v3/routing/solve`](/guides/vrp/v3/quickstart) |
| Score a plan you already have | [`/v3/routing/evaluate`](/guides/vrp/v3/evaluate) |
| Find slots for new tasks in a plan you keep | `/v3/routing/suggest` |

## The request

The body has three required parts:

<ParamField body="problem" type="object" required>
  The same `problem` object as on `/v3/routing/solve`: jobs, shipments, vehicles, depots, relations, objective, travel. It must contain the tasks already in the plan **and** the tasks you want to place.
</ParamField>

<ParamField body="plan" type="object" required>
  The committed plan, in the shape every solve and evaluate response returns as `plan`: one `routes[]` entry per vehicle shift, each with `vehicle`, `shift` and an ordered `stops[]` list. The tasks to place must not be in it. A route's `locked_count` marks stops that are already dispatched: no option lands before them.
</ParamField>

<ParamField body="options.tasks" type="array of strings" required>
  The ids of the jobs or shipments to place. At least one. Each must exist in `problem`, appear once, and be absent from `plan`.
</ParamField>

<ParamField body="options.max_suggestions" type="integer" default="5">
  Maximum number of ranked options per task. `0` returns every feasible placement. Positive values above `100` are lowered to `100`.
</ParamField>

<ParamField body="options.time_limit_s" type="integer">
  Search budget in seconds, at most `300`. `0` is raised to 100 ms. Omit it and the search runs to completion, which is what you want for a single job. The clock starts when the search starts, so fetching travel times does not eat into it.
</ParamField>

A task in `problem` that is in neither `plan` nor `options.tasks` is treated as unassigned context and gets no suggestions.

### Example

One truck already serves `delivery-1` and `delivery-2`. A third order, `delivery-3`, comes in and must be served between 08:00 and 11:00 local time.

```bash theme={null}
curl -X POST https://api.solvice.io/v3/routing/suggest \
  -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-01T12: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 }
        },
        {
          "id": "delivery-3",
          "location": { "coordinate": [4.4776, 51.0259] },
          "service_duration_s": 300,
          "time_windows": [
            { "from": "2026-04-01T08:00:00+02:00", "to": "2026-04-01T11:00:00+02:00" }
          ],
          "delivery": { "weight": 15 }
        }
      ],
      "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-1" },
            { "type": "job", "id": "delivery-2" }
          ]
        }
      ]
    },
    "options": {
      "tasks": ["delivery-3"],
      "max_suggestions": 3
    }
  }'
```

## The response

```json theme={null}
{
  "suggestions": [
    {
      "job_id": "delivery-3",
      "options": [
        {
          "vehicle": "truck-1",
          "position": 0,
          "before_job_id": "delivery-1",
          "extra_cost": 443860,
          "extra_distance_m": 22193,
          "extra_duration_s": 1705,
          "arrival": "2026-04-01T06:25:00Z",
          "latest_arrival": "2026-04-01T09:00:00Z"
        },
        {
          "vehicle": "truck-1",
          "position": 1,
          "after_job_id": "delivery-1",
          "before_job_id": "delivery-2",
          "extra_cost": 730228,
          "extra_distance_m": 5213,
          "extra_duration_s": 644,
          "arrival": "2026-04-01T07:31:30Z",
          "latest_arrival": "2026-04-01T09:00:00Z"
        }
      ]
    }
  ],
  "truncated": false,
  "elapsed_ms": 184
}
```

The figures are illustrative; yours depend on live travel times. Three positions exist on this route and only two come back: inserting `delivery-3` after `delivery-2` would reach it after its window closes, so that position is not feasible and is not listed.

`suggestions[]` has one entry per task you named, in the order the tasks appear in `problem` (jobs first, then shipments). Tasks tied together by a relation share one entry: see [Shipments and related tasks](#shipments-and-related-tasks).

<ResponseField name="suggestions[].job_id" type="string">
  The task this entry is about.
</ResponseField>

<ResponseField name="suggestions[].options" type="array">
  Feasible placements, cheapest first. Empty when there is none; `reason` then says why.
</ResponseField>

<ResponseField name="suggestions[].reason" type="string">
  Present only when `options` is empty. One of three fixed sentences: see [When a task gets no options](#when-a-task-gets-no-options).
</ResponseField>

<ResponseField name="truncated" type="boolean">
  One flag for the whole response. `true` when `options.time_limit_s` cut the search short for any task, or when an empty list for a multi-task unit is inconclusive. The lists may then be incomplete or miss the best option. A single job with no `time_limit_s` is never truncated.
</ResponseField>

<ResponseField name="elapsed_ms" type="integer">
  Wall-clock time of the whole request, in milliseconds.
</ResponseField>

### When a task gets no options

An empty `options` list is a `200`, not an error. The entry's `reason` is one of these three sentences:

| `reason` | What happened | What to do |
| - | - | - |
| `Search truncated before a feasible insertion was found` | `options.time_limit_s` ran out before any position was found. `truncated` is `true` | Raise `options.time_limit_s`, or omit it so the search runs to completion |
| ``No feasible insertion found within the beam; raise `max_suggestions` to search wider`` | For a unit of several stops, the search kept only the most promising partial placements and none of them could be completed. This does not prove that no placement exists. `truncated` is `true` | Raise `options.max_suggestions` |
| `No feasible insertion position found` | The search finished. No position in the committed plan satisfies the hard constraints | Relax the task or add capacity, or send the plan to `/v3/routing/solve` as a warm start so the other stops can move |

`reason` is written for people and logs. In code, branch on the empty `options` list and on `truncated`: when `truncated` is `false`, an empty list is final. `truncated` covers the whole response, so with several tasks in one request it does not say which task was cut short; send one task per request when you need that.

### Reading an option

| Field | Meaning |
| - | - |
| `vehicle` | The vehicle that would take the task |
| `shift_index` | 0-based index into that vehicle's `shifts[]`. Omitted when `0` |
| `trip_index` | Which trip of that shift, on a route with reloads: `0` is the first trip, `n` the trip that starts at the route's `n`-th `reload` stop. Omitted when `0` |
| `position` | 0-based index among the task stops of that trip. Start, end, break and reload stops do not count |
| `after_job_id` | The task directly before the new stop. Absent when the new stop comes first in its trip |
| `before_job_id` | The task directly after the new stop. Absent when the new stop comes last in its trip |
| `extra_cost` | How much the plan's cost rises with this placement. An integer in units of 1/100,000 of your currency: `443860` is 4.4386. Options are ranked on it |
| `extra_distance_m` | Distance added to the routes this placement touches, in metres |
| `extra_duration_s` | Travel plus service time added to those routes, in seconds |
| `arrival` | When the vehicle would arrive at the new stop (UTC) |
| `latest_arrival` | The latest arrival at the new stop that keeps the rest of the route feasible (UTC) |
| `interchangeable_vehicles` | Other unused vehicles that would serve the task identically. Present only when `vehicle` has no other stops |
| `assignments` | For a unit of several stops, every stop of the unit with its own vehicle, position, neighbours and arrival. Absent for a single job |

<Note>
  `extra_cost` and `extra_duration_s` measure different things. In the example the first option adds more driving than the second, yet costs less: the truck used to wait at `delivery-1` for the window to open, and the detour fits inside that wait, so the route does not end any later. See [Objective function](/guides/vrp/v3/concepts/scoring-explanation) for what the cost is made of.
</Note>

## Applying a suggestion

Suggest only reports options. To act on one, edit the plan yourself and send it back:

<Steps>
  <Step title="Find the route">
    Take the option's `vehicle` and the shift at `shift_index` in that vehicle's `shifts[]`, and find the `plan.routes[]` entry with that `vehicle` and `shift` id. If the vehicle was unused, add a new route entry for it.
  </Step>

  <Step title="Insert the stop">
    Insert `{ "type": "job", "id": "<job_id>" }` so that it becomes task stop number `position` of the trip. On a route without reloads that is index `position` in `stops`. For the first option above, `plan` becomes:

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

  <Step title="Confirm or re-optimise">
    Send the edited plan to [`/v3/routing/evaluate`](/guides/vrp/v3/evaluate) to get the full timeline and cost of the new plan, or to `/v3/routing/solve` as a warm start if the solver may also rearrange the other stops.
  </Step>
</Steps>

Each task is ranked on its own against the same committed plan. If you name two tasks and apply an option for the first, the options for the second were computed without it: call suggest again with the updated plan.

## Shipments and related tasks

* **Shipments.** Name the shipment id in `options.tasks`. Its pickup and delivery are placed together: the option's top-level fields describe the pickup, and `assignments[]` lists both legs with a `type` of `pickup` or `delivery`. Either both legs are in `plan` or neither is.
* **`ordered`, `same_resource` and `same_day` relations.** Tasks tied together by one of these relations are placed as one unit and get one entry, named after the first new task of the unit.
* **`synchronized` and `time_lag` relations.** A task that takes part in one of these cannot be suggested; the request is rejected with a `400`.
* **Reloads.** An option can land in any existing trip of a multi-trip route, but suggest never adds a new reload trip.

## Errors

Suggest returns the same [error body](/guides/vrp/v3/errors) as the other endpoints. The cases specific to it:

| Situation | Result |
| - | - |
| `options.tasks` is empty | `400`, item `invalid_value`, pointer `/options/tasks` |
| A task id is listed twice | `400`, item `duplicate_id`, pointer `/options/tasks/{k}` |
| A task id is not in `problem` | `400`, item `unknown_reference`, pointer `/options/tasks/{k}` |
| A task is already in `plan` | `400`, item `conflicting_fields`, pointer `/options/tasks/{k}` |
| `options.time_limit_s` above `300` | `400`, item `limit_exceeded`, pointer `/options/time_limit_s` |
| A named task is in a `synchronized` or `time_lag` relation | `400` |
| A shipment has one leg in `plan` and the other missing | `400` |
| The plan does not fit the problem (unknown vehicle, shift or stop id, a stop listed twice) | `400`, pointer `/plan/routes/{i}` or `/plan/routes/{i}/stops/{k}` |
| No feasible position for a task | `200`, with an empty `options` list and a [`reason`](#when-a-task-gets-no-options) |

`?validate_only=true` checks `problem` and `options` without fetching travel times or searching. It does not check that `plan` fits the problem; only the real call does.

<Columns cols={2}>
  <Card title="Evaluate" icon="scale-balanced" href="/guides/vrp/v3/evaluate">
    Score the plan after you apply an option.
  </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="The V3 API model" icon="cube" href="/guides/vrp/v3/api-design">
    The `problem` and `plan` objects in full.
  </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.