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

# Async runs

> Solve in the background: submit a run, poll its status, fetch the plan or cancel it

<Info>
  **V3 is in beta.** General availability (GA) is planned for January 2027.
</Info>

## Overview

`POST /v3/routing/solve` holds the connection open until the plan is ready and caps the search at 300 seconds. An **async run** takes the same request body, answers at once, and solves in the background, with a search of up to 3,600 seconds. You then poll the run and fetch the plan when it is done.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/v3/routing/runs` | Submit a run. Answers `202` with the run |
| `GET` | `/v3/routing/runs/{id}` | Poll the run's status |
| `GET` | `/v3/routing/runs/{id}/result` | Fetch the plan of a `succeeded` run |
| `POST` | `/v3/routing/runs/{id}/cancel` | Cancel a `queued` or `running` run |

Only solve runs in the background. Evaluate and suggest are quick, and stay synchronous.

All four are served from `https://api.solvice.io` and take your API key in the `Authorization` header, as on the other endpoints. A run belongs to your account: another account's run id is a `404`.

## The life of a run

```text theme={null}
queued ──► running ──► succeeded
   │          │
   │          └──────► failed
   └─────────────────► cancelled   (from queued or running)
```

| `status` | Meaning |
| - | - |
| `queued` | Accepted, waiting for a worker to start |
| `running` | A worker is solving it. `started_at` is set |
| `succeeded` | The plan is ready at `/result` |
| `failed` | The run ended without a plan. `error` says why |
| `cancelled` | You cancelled it. It never returns a plan |

`succeeded`, `failed` and `cancelled` are final.

## Submit a run

Send the body you would send to [`/v3/routing/solve`](/guides/vrp/v3/quickstart): a `problem`, and optionally a `plan` and `options`. These differ from solve:

* `options.time_limit_s` may be up to `3600` (solve: `300`).
* `callback` is not supported yet. A body that has it is a `400` with the item code `not_supported` at `/callback`.
* The request is validated before the run is accepted, with the same checks and the same [error body](/guides/vrp/v3/errors) as solve, so a bad body is a `400` here and never a `failed` run later.

```bash theme={null}
curl -X POST https://api.solvice.io/v3/routing/runs \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

The answer is `202` with the run. Keep its `id`:

```json 202 Accepted theme={null}
{
  "id": "d28568b1-34cc-4f89-b10b-dc24b26a8275",
  "status": "queued",
  "links": {
    "self": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275",
    "result": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result"
  }
}
```

Send an `X-Request-Id` header of 1 to 128 visible ASCII characters to trace the call. It is echoed in the `x-request-id` response header, and a generated `req_…` id is used when it is absent or malformed.

## The run

Every endpoint but `/result` answers with the run:

<ResponseField name="id" type="string (UUID)">
  The run id, for the paths above.
</ResponseField>

<ResponseField name="status" type="string">
  `queued`, `running`, `succeeded`, `failed` or `cancelled`. See [The life of a run](#the-life-of-a-run).
</ResponseField>

<ResponseField name="links" type="object">
  `self` and `result`: the paths to poll and to fetch the plan from, relative to the API host.
</ResponseField>

<ResponseField name="error" type="object">
  On `failed` only: `status`, `code`, `message` and, for an invalid request, `errors[]`, in the shape of the [error body](/guides/vrp/v3/errors). The `code` is `invalid_request`, `matrix_provider_error`, `internal` or `worker_lost`.
</ResponseField>

<ResponseField name="started_at" type="string (RFC 3339, UTC)">
  When a worker claimed the run. Absent while it is `queued`.
</ResponseField>

<ResponseField name="finished_at" type="string (RFC 3339, UTC)">
  When the run became final. Absent before then.
</ResponseField>

<ResponseField name="expires_at" type="string (RFC 3339, UTC)">
  When the run and its plan are deleted: `finished_at` plus 365 days. Absent before the run is final.
</ResponseField>

## Poll a run

```bash theme={null}
curl https://api.solvice.io/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275 \
  -H "Authorization: YOUR_API_KEY"
```

Poll every few seconds until `status` is final. While the worker runs:

```json 200 OK, running theme={null}
{
  "id": "d28568b1-34cc-4f89-b10b-dc24b26a8275",
  "status": "running",
  "links": {
    "self": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275",
    "result": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result"
  },
  "started_at": "2026-10-15T07:40:59Z"
}
```

And when it is done:

```json 200 OK, succeeded theme={null}
{
  "id": "d28568b1-34cc-4f89-b10b-dc24b26a8275",
  "status": "succeeded",
  "links": {
    "self": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275",
    "result": "/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result"
  },
  "started_at": "2026-10-15T07:40:59Z",
  "finished_at": "2026-10-15T07:41:00Z",
  "expires_at": "2027-10-15T07:41:00Z"
}
```

A run that stops without saying so does not stay `running` for ever. Each poll checks the worker, and the run turns `failed` with `error.code` `worker_lost` when the worker is gone or the run is older than 6,000 seconds. A run that is still `queued` after 30 minutes turns `failed` with `internal`.

## Fetch the plan

```bash theme={null}
curl https://api.solvice.io/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/result \
  -H "Authorization: YOUR_API_KEY"
```

For a `succeeded` run the answer is `200` with exactly the response `POST /v3/routing/solve` returns: `summary`, `routes`, `plan` and `unassigned`. See [The V3 API model](/guides/vrp/v3/api-design#response) for every field, and send `plan` back in a later request to re-optimise. The plan is kept until `expires_at`.

For any other status `/result` answers `409`:

| `message` starts with | When |
| - | - |
| `run_not_finished` | The run is `queued` or `running`. Poll again |
| `no_result` | The run `failed` or was `cancelled`. Read `error` on the run, or submit again |

## Cancel a run

```bash theme={null}
curl -X POST https://api.solvice.io/v3/routing/runs/d28568b1-34cc-4f89-b10b-dc24b26a8275/cancel \
  -H "Authorization: YOUR_API_KEY"
```

The answer is `200` with the run: `cancelled`, or unchanged when it had already finished, since finishing wins a race. A run is cancelled whether it is `queued` or `running`.

```json 200 OK, cancelled theme={null}
{
  "id": "c77fa298-1754-4539-b64b-9ee730479f7c",
  "status": "cancelled",
  "links": {
    "self": "/v3/routing/runs/c77fa298-1754-4539-b64b-9ee730479f7c",
    "result": "/v3/routing/runs/c77fa298-1754-4539-b64b-9ee730479f7c/result"
  },
  "finished_at": "2026-10-15T07:41:22Z",
  "expires_at": "2027-10-15T07:41:22Z"
}
```

Cancelling does not stop the worker. A cancelled run keeps counting toward the limit of 10 runs until its worker stops, and its plan is never served: `/result` is a `409` with `no_result`.

## Limits

| Limit | Value | Result |
| - | - | - |
| `options.time_limit_s` | up to `3600` | `400`, item `limit_exceeded`, pointer `/options/time_limit_s` |
| Queued or running runs per account | 10 | `429` with `Retry-After: 30` |
| Request body | 10 MiB after decompression | `413` |
| Retention | 365 days after the run is final | `expires_at` |

The other [request limits](/guides/vrp/v3/limits) apply as on solve. Over the rate limit the answer is a `429` as on every endpoint.

## Errors

A bad request body is answered with the [error body](/guides/vrp/v3/errors) of solve, with the same `400`, `403`, `413` and `415` cases. The gateway answers the rest in its own shape, so branch on the HTTP status first:

| Status | When | Body |
| - | - | - |
| `400` | Submit: the request is invalid. Any run endpoint: `id` is not a UUID | submit: the error body. Bad `id`: `{"error": "Invalid UUID string: …"}` |
| `401` | Missing or invalid API key | Possibly empty |
| `404` | No run with this id on your account | `{"message": "not_found: routing run …", "error": "GATEWAY_ERROR"}` |
| `409` | `/result` of a run without a plan | `{"message": "run_not_finished: …", "error": "GATEWAY_ERROR"}` or `no_result` |
| `429` | Over the rate limit, or 10 runs already queued or running | `Retry-After` header |
| `503` | The run could not be started. Nothing was created: submit again | `{"message": "Failed to launch routing run", "error": "GATEWAY_ERROR"}` |

Each of these has a `message` that begins with a code, then a colon and a sentence. Never parse the sentence.

## Next steps

<CardGroup cols={2}>
  <Card title="The V3 API model" icon="cube" href="/guides/vrp/v3/api-design">
    The request, and the plan a run returns
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/guides/vrp/v3/errors">
    The error body and every code
  </Card>
</CardGroup>


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