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

# First API call

> Get your API token, submit a minimal plan with one vehicle and three orders, then retrieve the optimized route the engine computes for it, in under 15 minutes.

Kardinal's Route Optimization API is built around two steps: you submit a **plan** (`resources` such as vehicles, `orders`, and constraints), and you retrieve the **solution** the engine computes for it. This tutorial walks through both, using the smallest possible plan.

> [!NOTE]
>
> Access to the API is self-service — see [Authentication and API keys](/guides/authentication) for how to sign up and what account structure it creates.

> [!TIP]
>
> Working in Python or TypeScript? The [SDK quickstart](/sdks/quickstart) gets you to the same result in a few lines, with typed models and no HTTP code to write.

## Step 1 — Authenticate

Kardinal uses JWT authentication. Every request needs a **long-term API token**, which you generate once from the [Console](https://console.kardinal.ai) and reuse as a bearer token for as long as it's valid.

> **[Get your API key](https://console.kardinal.ai/keys)**
>
> Sign up or log in to the Console, then generate your token under **API Keys**.

Log in to the [Console](https://console.kardinal.ai), then go to **API Keys** in the sidebar to generate your token.

Keep it — every following request in this tutorial uses it as a bearer token. It's valid for **366 days**; see [Authentication and API keys](/guides/authentication) for the full authentication model.

## Step 2 — Submit a minimal plan

A plan is created with `POST /plans` — the service generates the plan's `id`, returned in the response; an `id` sent in this request is ignored and replaced by a generated one. Here is the smallest viable plan: one resource and three pickup-only orders in Paris.

```bash theme={null}
curl -X POST "https://app.kardinal.ai/api/v2/plans" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
  "tz": "Europe/Paris",
  "resources": [
    {
      "id": "resource1",
      "vehicleProfile": { "type": "fly", "kmph": 20 },
      "workingTimeWindow": { "begin": "2026-03-21 08:00", "end": "2026-03-21 23:00" }
    }
  ],
  "orders": [
    { "id": "order-1", "stops": [{ "type": "single", "id": "Balard", "position": { "lon": 2.279424, "lat": 48.835749 }, "kind": "pickup", "operationDuration": "PT5M30S" }] },
    { "id": "order-2", "stops": [{ "type": "single", "id": "Dauphine", "position": { "lon": 2.274264, "lat": 48.870087 }, "kind": "pickup", "operationDuration": "PT5M30S" }] },
    { "id": "order-3", "stops": [{ "type": "single", "id": "Station-f", "position": { "lon": 2.370564, "lat": 48.83476 }, "kind": "pickup", "operationDuration": "PT5M30S" }] }
  ],
  "maxOptimizationDuration": "PT1M"
}'
```

The `"tz"` field gives the time zone of the plan. Datetimes written as a local time with a space and no offset, such as `2026-03-21 08:00`, are read as local time in that zone; see [Dates and time zones](/reference/data-model#dates-and-time-zones).

```json Response (abridged) theme={null}
{
  "item": {
    "id": "<plan_id>",
    "resources": [ ... ],
    "orders": [ ... ]
  }
}
```

Keep the `id` from the response — every request below uses it in place of `{planId}`.

> [!NOTE]
>
> `maxOptimizationDuration` and other duration fields use [ISO 8601 duration syntax](https://en.wikipedia.org/wiki/ISO_8601#Durations): `PT1M` reads as "period, time, 1 minute."

> [!TIP]
>
> `"type": "fly"` is a crow-fly vehicle profile — it's fast to compute and ideal for a first test. To see real road-network routing instead, replace it with `"vehicleProfile": { "type": "car" }` (no `kmph` needed) and re-run the same request with a production token — the response shape is identical. The sandbox agency computes every route crow-fly, whatever the vehicle profile, so road-network routing only shows up in production. Real integrations typically use `car` or `truck` profiles (see the data model reference).

As soon as the plan is accepted, optimization starts automatically — there's no separate "start" call.

## Step 3 — Retrieve the solution

```bash theme={null}
curl "https://app.kardinal.ai/api/v2/plans/{planId}/solution" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <token>"
```

The response wraps the result in an `item` field and gives you, per resource, the ordered list of stops (`tours[].wayPoints`) with arrival/departure times, plus any stop that couldn't be planned:

```json Response (abridged) theme={null}
{
  "item": {
    "planId": "<plan_id>",
    "planVersion": 1,
    "unaffectedStopIds": [],
    "unusedResourceIds": [],
    "tours": [
      {
        "resourceId": "resource1",
        "distanceInKm": 10.509,
        "workingDuration": "PT48M2S",
        "wayPoints": [
          { "type": "stop", "stopId": "Station-f", "arrivalTime": "2026-03-21T08:00:00+01:00", "stopKind": "pickup" },
          { "type": "stop", "stopId": "Balard", "arrivalTime": "2026-03-21T08:25:31+01:00", "stopKind": "pickup" },
          { "type": "stop", "stopId": "Dauphine", "arrivalTime": "2026-03-21T08:42:32+01:00", "stopKind": "pickup" }
        ]
      }
    ]
  }
}
```

The timings follow from the plan itself: at the `fly` profile's 20 km/h, the 10.5 km tour takes about 31 minutes of travel, plus 5 minutes 30 seconds at each of the three stops.

`unaffectedStopIds` lists stops that couldn't be planned within hard constraints, and `unusedResourceIds` lists resources left with nothing assigned — both empty here since this minimal plan has no constraints to violate. See [Why a stop, not always the problem, becomes infeasible](/concepts/hard-vs-soft-constraints#why-a-stop-not-always-the-problem-becomes-infeasible) for the full picture.

For a small test plan like this one, the solution is typically ready within seconds — for larger plans, poll `GET /plans/{planId}/state` until it returns `state: "optimized"` — the plan goes through states such as `waiting`, `processing`, and `optimizing` first (see [Polling the state and the solution](/concepts/how-the-optimization-engine-works#polling-the-state-and-the-solution) for the polling interval, the other end states, and why an early solution can be empty).

## Next steps

* `PUT /plans/{planId}` with the complete plan (the same resources and orders, plus your change) to see interactive re-optimization in action — read [How the optimization engine works](/concepts/how-the-optimization-engine-works#continuous-and-interactive-optimization) first to understand what happens on update.
* Add time windows, capacities, and skills to your orders and resources — see the full data model reference.
* Move from crow-fly (`fly`) to a real vehicle profile (`car`, `truck`) before going further than a smoke test — with a production token, since the sandbox always computes crow-fly.
* Size `maxOptimizationDuration` for your real volumes: `PT1M` only suits this three-stop test. See [Sizing `maxOptimizationDuration` for a large problem](/guides/handling-large-volumes#sizing-maxoptimizationduration-for-a-large-problem).

> [!WARNING]
>
> **Modeling a real client's data next, not just this tutorial?** Read [Start here if you're an AI agent](/getting-started/agent-modeling-checklist) before writing that integration — it covers the checks and decisions that most commonly get skipped.


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