> ## 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 a sandbox key and calculate your first route 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.
</Note>

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

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 '{
  "resources": [
    {
      "id": "resource1",
      "vehicleProfile": { "type": "fly", "kmph": 20 },
      "workingTimeWindow": { "begin": "2026-03-21T08:00:00Z", "end": "2026-03-21T23:00:00Z" }
    }
  ],
  "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"
}'
```

```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."
</Note>

<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).
</Tip>

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:00Z", "stopKind": "pickup" },
          { "type": "stop", "stopId": "Balard", "arrivalTime": "2026-03-21T08:25:31Z", "stopKind": "pickup" },
          { "type": "stop", "stopId": "Dauphine", "arrivalTime": "2026-03-21T08:42:32Z", "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.
</Warning>


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