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

# Python SDK reference

> Every public class, method, and exception of the Kardinal Python SDK (kardinal-ai): signatures, the API endpoint behind each call, return values, and errors.

Reference for `kardinal-ai` 1.0.0. Install it with `pip install kardinal-ai`, import it as `kardinal`. Every method below is synchronous. Field-level documentation for plans and solutions lives in the [API Reference](/api-reference/plan/create-a-plan).

## Client

### `Kardinal`

```python theme={null}
Kardinal(api_key=None, *, base_url=None, timeout=30, max_retries=3)
```

| Parameter | Type | Default | Description |
| - | - | - | - |
| `api_key` | `str` | `KARDINAL_API_KEY` | Your API key. Raises `MissingApiKeyError` if none is found, `EmptyApiKeyError` for `""`. |
| `base_url` | `str` | `KARDINAL_BASE_URL`, then `https://app.kardinal.ai/api/v2` | API host. Leave unset. |
| `timeout` | `float` | `30` | Seconds, applied by `httpx` to each phase of a request. |
| `max_retries` | `int` | `3` | Retries of a read, `update`, `delete`, `start`, or `stop` after a network error, a `429`, or a `5xx`. |

The methods hang off `k.routing.plans` and `k.routing.solutions`. The client holds an `httpx` connection pool: use it as a context manager (`with Kardinal() as k:`) or call `k.close()`. See [Client configuration](/sdks/configuration).

## Plans

### `plans.submit`

```python theme={null}
k.routing.plans.submit(plan: PlanCreate, *, allow_unknown: Collection[str] = ()) -> Plan
```

[`POST /plans`](/api-reference/plan/create-a-plan). Creates the plan and starts its optimization. Returns the stored `Plan`, with the `id` the server assigned and `.warnings`. Raises `UnknownFieldError`, before sending, for a key no model declares, unless named in `allow_unknown`. Consumes credits with a production key. **Never retried.**

### `plans.get`

```python theme={null}
k.routing.plans.get(plan_id: str) -> Plan
```

[`GET /plans/{planId}`](/api-reference/plan/retrieve-a-plan). Returns the plan as currently stored. Raises `KardinalNotFoundError` if it doesn't exist.

### `plans.update`

```python theme={null}
k.routing.plans.update(plan: Plan, *, force: bool = False, allow_unknown: Collection[str] = ()) -> Plan
```

[`PUT /plans/{planId}`](/api-reference/plan/update-a-plan). Replaces the whole plan: anything left out is removed. The version increments, and the plan is re-optimized, only if the content changed; an identical update is a free no-op. Fields unknown to this SDK version that `get` read are sent back untouched; a key you added that no model declares raises `UnknownFieldError`, unless named in `allow_unknown`. On a plan not read from the API, a likely typo only emits an `UnknownFieldWarning`. `force=True` unarchives an archived plan first. Retried.

### `plans.delete`

```python theme={null}
k.routing.plans.delete(plan_id: str) -> None
```

[`DELETE /plans/{planId}`](/api-reference/plan/delete-a-plan). Permanently deletes the plan and its solution, and stops any optimization in progress. Retried.

### `plans.list`

```python theme={null}
k.routing.plans.list(*, page: int = 1, items_per_page: int = 20, archived: Literal["excluded", "included", "only"] = "excluded") -> Page[PlanLight]
```

[`GET /plans`](/api-reference/plan/retrieve-the-list-of-plans-in-a-light-representation). Returns one page of plans, in their light form. A page past the last one returns the last page.

### `plans.iter_all`

```python theme={null}
k.routing.plans.iter_all(*, archived: Literal["excluded", "included", "only"] = "excluded") -> Iterator[PlanLight]
```

`GET /plans`, every page. Iterates over every plan, fetching pages as it goes.

### `plans.start`

```python theme={null}
k.routing.plans.start(plan_id: str, *, force: bool = False) -> EnvelopedPlanReference
```

[`PUT /plans/{planId}/running`](/api-reference/plan/stop-or-restart-the-optimization-of-a-plan). Restarts the optimization of the current version, without changing its content or version. Returns `agencyId`, `planId`, `planVersion`, and `warnings`. Retried.

### `plans.stop`

```python theme={null}
k.routing.plans.stop(plan_id: str, *, force: bool = False) -> EnvelopedPlanReference
```

[`PUT /plans/{planId}/running`](/api-reference/plan/stop-or-restart-the-optimization-of-a-plan). Stops the optimization in progress, without changing the plan's content or version. Retried.

### `plans.get_status`

```python theme={null}
k.routing.plans.get_status(plan_id: str) -> PlanState | str
```

[`GET /plans/{planId}/state`](/api-reference/plan/retrieve-the-latest-state-of-a-plan). Returns the plan's current state, a single value: `waiting`, `processing`, `optimizing`, `optimized`, `stopped`, `interrupted`, and so on. A state this SDK version doesn't know comes back as its raw string. Not the detailed `PlanStatus` object found in `Plan.status`.

## Solutions

### `solutions.wait`

```python theme={null}
k.routing.solutions.wait(plan_id: str, timeout: float | None = None) -> Solution
```

`GET /plans/{planId}/state`, then [`/solution`](/api-reference/solution/retrieve-a-plan-solution). Blocks until the optimization of the current version is over, then returns its solution. Reads the state every 10 seconds at first, up to every 60 seconds. `solution.final_state` tells how it ended: `optimized`, `interrupted`, or `stopped`. Raises `KardinalOptimizationError` (`NO_SOLUTION`, `PLAN_SUPERSEDED`, `OPTIMIZATION_STALLED`), `KardinalNotFoundError` if the plan is deleted meanwhile, and `KardinalTimeoutError` once `timeout` seconds have elapsed. See [Waiting for the solution](/sdks/plan-lifecycle#waiting-for-the-solution).

### `solutions.get`

```python theme={null}
k.routing.solutions.get(plan_id: str) -> Solution
```

[`GET /plans/{planId}/solution`](/api-reference/solution/retrieve-a-plan-solution). Returns the current best solution right away, without waiting. Raises `KardinalNotFoundError` until the engine has a first solution.

### `solutions.get_objectives`

```python theme={null}
k.routing.solutions.get_objectives(plan_id: str) -> SolutionObjectives
```

[`GET /plans/{planId}/solution/objectives`](/api-reference/solution/retrieve-the-objectives-of-a-plan-solution). Returns the value of each objective, without the tours: a plain `list` of `SolutionObjective`, with a `.warnings` attribute.

## Examples

### `examples.small_delivery_plan`

```python theme={null}
from kardinal.routing import examples

examples.small_delivery_plan() -> PlanCreate
```

A ready-to-submit plan: two resources, eight single-stop orders around central Paris, and a 10-second optimization. The working window starts tomorrow at 8:00. Each call returns a new object.

## Local checks

### `unknown_fields`

```python theme={null}
from kardinal.routing import unknown_fields

unknown_fields(plan) -> list[UnknownField]
```

Runs the check `submit` makes, without sending anything: every key no model declares, with its path and, when one is close, the real field name it was likely meant to be. Empty when the plan is clean.

## Types

### `PlanCreate`

From `kardinal.routing.plans`. The plan you submit: no `id`, the server assigns it. Built from `kardinal.routing.models` objects, or loaded from JSON with `PlanCreate.model_validate_json(text)`. Its orders are `kardinal.routing.plans.Order` objects.

### `Plan`

From `kardinal.routing.plans`. The plan as stored, returned by `submit`, `get`, and `update`: the same content plus `id`, `version`, `status`, `state`, and `agencyId`, and `.warnings` from the call that returned it.

### `Page`

From `kardinal`. Returned by `plans.list`: `items`, `page`, `total_pages`, `total`, `next_page` (`None` on the last page), `previous_page` (`None` on the first page), and `items_per_page`.

### Model types

`kardinal.routing.models` holds a `pydantic` model for every schema of the [API specification](/api-reference/plan/create-a-plan) (`Resource`, `SingleStop`, `AlternativesStop`, `Position`, `TimeWindow`, `Solution`, `Tour`, `Warning`, and so on), with the API's field names and descriptions.

## Exceptions

All importable from `kardinal`, all subclasses of `KardinalError`. See [Errors, retries, and warnings](/sdks/errors-and-warnings).

| Exception | Raised on |
| - | - |
| `KardinalAuthError` | `401` |
| `KardinalLimitError` | Any `403`: plan-size limit, quota, or credits |
| `KardinalValidationError` | `400` |
| `KardinalNotFoundError` | `404` |
| `KardinalRateLimitError` | `429` |
| `KardinalServerError` | `5xx` |
| `KardinalConnectionError` | No HTTP response |
| `KardinalTimeoutError` | `solutions.wait` time limit elapsed |
| `KardinalOptimizationError` | `solutions.wait` has no solution to return |
| `UnknownFieldError` | Unknown key found before sending |
| `MissingApiKeyError`, `EmptyApiKeyError` | No usable key when creating the client |

`UnknownFieldWarning`, also importable from `kardinal`, is a warning, not an exception: see [Client-side errors](/sdks/errors-and-warnings#client-side-errors).

Every exception carries `.code`, `.status`, `.errors`, `.details`, `.retryable`, `.warnings`, and `.request_id`.

## Known limitations

* The client is synchronous; there's no `asyncio` client.
* `pydantic` converts a string holding a number or a boolean (`"1"`, `"true"`) instead of refusing it.
* `.request_id` is usually `None`: the API sends an `X-Request-Id` header only on some responses.
* The `kardinal` command needs the `cli` extra: `pip install "kardinal-ai[cli]"`.


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