# Quickstart

> Create your first test delivery: pick a vehicle type, quote a route, book it, and follow the simulated courier.

This guide books a delivery from Köln to Düsseldorf with a test key. Test deliveries are never dispatched or billed. A simulated courier, the Robocourier, moves them through every status.

## Before you start

You need a business or fleet workspace in Maxmove in which you are an owner or admin. Only owners and admins can create API keys.

## Create a test delivery

**1. Create a test key**

In the [dashboard](https://dashboard.maxmove.com), open **Settings → API keys** and select **Create API key**. Choose **Test**, keep the permissions you need, and copy the key. It starts with `mm_test_` and is shown only once.

Store it in an environment variable:

```bash
export MAXMOVE_KEY="mm_test_…"
```

See [Authentication](https://maxmove.com/en/developers/docs/authentication) for permissions and expiration.

**2. Pick a vehicle type**

List the vehicle types you can book. Pass an `id` from the response as `vehicle_type_id` in the next steps. The examples use `courier`.

```bash cURL
curl https://api.maxmove.com/v1/vehicle-types \
  -H "x-api-key: $MAXMOVE_KEY"
```

```ts Node.js
const BASE_URL = 'https://api.maxmove.com/v1';
const headers = {
  'x-api-key': process.env.MAXMOVE_KEY!,
  'content-type': 'application/json',
};

const res = await fetch(`${BASE_URL}/vehicle-types`, { headers });
const { vehicle_types } = await res.json();
console.log(vehicle_types.map((type: { id: string }) => type.id));
```

```python Python
import os
import requests

BASE_URL = "https://api.maxmove.com/v1"
headers = {"x-api-key": os.environ["MAXMOVE_KEY"]}

res = requests.get(f"{BASE_URL}/vehicle-types", headers=headers)
print([vehicle_type["id"] for vehicle_type in res.json()["vehicle_types"]])
```

**3. Price the route**

Send pickup and dropoff with their coordinates. Maxmove does not geocode, so send the coordinates your address search returned. The quote is valid for about 5 minutes.

```bash cURL
curl -X POST https://api.maxmove.com/v1/quotes \
  -H "x-api-key: $MAXMOVE_KEY" -H "content-type: application/json" \
  -d '{
    "pickup":  { "address": "Ehrenstraße 15, 50672 Köln", "latitude": 50.9385, "longitude": 6.9469 },
    "dropoff": { "address": "Königsallee 27, 40212 Düsseldorf", "latitude": 51.2233, "longitude": 6.7768 },
    "vehicle_type_id": "courier"
  }'
```

```ts Node.js
const pickup = { address: 'Ehrenstraße 15, 50672 Köln', latitude: 50.9385, longitude: 6.9469 };
const dropoff = { address: 'Königsallee 27, 40212 Düsseldorf', latitude: 51.2233, longitude: 6.7768 };

const quoteRes = await fetch(`${BASE_URL}/quotes`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ pickup, dropoff, vehicle_type_id: 'courier' }),
});
const quote = await quoteRes.json();
console.log(quote.id, quote.amount_cents, quote.currency, quote.valid_until);
```

```python Python
pickup = {"address": "Ehrenstraße 15, 50672 Köln", "latitude": 50.9385, "longitude": 6.9469}
dropoff = {"address": "Königsallee 27, 40212 Düsseldorf", "latitude": 51.2233, "longitude": 6.7768}

quote = requests.post(
    f"{BASE_URL}/quotes",
    headers=headers,
    json={"pickup": pickup, "dropoff": dropoff, "vehicle_type_id": "courier"},
).json()
print(quote["id"], quote["amount_cents"], quote["currency"], quote["valid_until"])
```

The response contains the quote `id` (`qt_…`), the price in `amount_cents` and `currency`, and `valid_until`.

**4. Book the delivery**

Send the same route, the `quote_id`, and who the courier meets at each end. The `Idempotency-Key` header is required: if the request times out, retry it with the same key and body, and you get the same delivery instead of a second one.

```bash cURL
curl -X POST https://api.maxmove.com/v1/deliveries \
  -H "x-api-key: $MAXMOVE_KEY" -H "content-type: application/json" \
  -H "Idempotency-Key: order-4711" \
  -d '{
    "quote_id": "qt_…",
    "pickup": {
      "address": "Ehrenstraße 15, 50672 Köln", "latitude": 50.9385, "longitude": 6.9469,
      "contact": { "name": "Feinkost Ehrenfeld", "phone": "+49 221 1234567" }
    },
    "dropoff": {
      "address": "Königsallee 27, 40212 Düsseldorf", "latitude": 51.2233, "longitude": 6.7768,
      "contact": { "name": "Erika Muster", "phone": "+49 170 1234567" },
      "instructions": "Ring at reception, 2nd floor."
    },
    "vehicle_type_id": "courier",
    "external_id": "order-4711"
  }'
```

```ts Node.js
const deliveryRes = await fetch(`${BASE_URL}/deliveries`, {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': 'order-4711' },
  body: JSON.stringify({
    quote_id: quote.id,
    pickup: {
      ...pickup,
      contact: { name: 'Feinkost Ehrenfeld', phone: '+49 221 1234567' },
    },
    dropoff: {
      ...dropoff,
      contact: { name: 'Erika Muster', phone: '+49 170 1234567' },
      instructions: 'Ring at reception, 2nd floor.',
    },
    vehicle_type_id: 'courier',
    external_id: 'order-4711',
  }),
});
const delivery = await deliveryRes.json();
console.log(deliveryRes.status, delivery.id, delivery.status);
```

```python Python
delivery = requests.post(
    f"{BASE_URL}/deliveries",
    headers={**headers, "Idempotency-Key": "order-4711"},
    json={
        "quote_id": quote["id"],
        "pickup": {
            **pickup,
            "contact": {"name": "Feinkost Ehrenfeld", "phone": "+49 221 1234567"},
        },
        "dropoff": {
            **dropoff,
            "contact": {"name": "Erika Muster", "phone": "+49 170 1234567"},
            "instructions": "Ring at reception, 2nd floor.",
        },
        "vehicle_type_id": "courier",
        "external_id": "order-4711",
    },
).json()
print(delivery["id"], delivery["status"])
```

A new delivery answers `201` with status `pending`. If the route differs from the quoted route, the request fails with `400 quote_mismatch`; if the quote has expired, with `400 quote_expired`. Request a new quote and retry.

**5. Follow the delivery**

Read the tracking snapshot. The Robocourier advances the delivery one status about every 20 seconds until it is `delivered`.

```bash cURL
curl https://api.maxmove.com/v1/deliveries/del_…/tracking \
  -H "x-api-key: $MAXMOVE_KEY"
```

```ts Node.js
const trackingRes = await fetch(`${BASE_URL}/deliveries/${delivery.id}/tracking`, { headers });
const tracking = await trackingRes.json();
console.log(tracking.status, tracking.eta_seconds, tracking.tracking_url);
```

```python Python
tracking = requests.get(
    f"{BASE_URL}/deliveries/{delivery['id']}/tracking", headers=headers
).json()
print(tracking["status"], tracking["eta_seconds"], tracking["tracking_url"])
```

Instead of polling, [register a webhook endpoint](https://maxmove.com/en/developers/docs/webhooks/events) to get an event for every change.

## Go live

When your integration works in test mode, create a live key (`mm_live_…`) and swap it in. The base URL and requests stay the same. Live deliveries are dispatched to real couriers and billed to your workspace on its monthly invoice, so your workspace must be approved for invoice billing first. See [Billing](https://maxmove.com/en/developers/docs/billing).

Webhook endpoints belong to one mode: register your endpoints again with the live key.

## Next steps

  - [Delivery lifecycle](https://maxmove.com/en/developers/docs/concepts/deliveries): Statuses, cancellation, tracking, and proof of delivery.
  - [Webhooks](https://maxmove.com/en/developers/docs/webhooks/events): Get events instead of polling.
  - [Test mode](https://maxmove.com/en/developers/docs/test-mode): Simulate cancellations and failed deliveries.
  - [Errors](https://maxmove.com/en/developers/docs/concepts/errors): Error codes and how to handle them.

---

Source: https://maxmove.com/en/developers/docs/quickstart
