> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.goautolane.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.goautolane.com/_mcp/server.

# Direct Delivery

> API reference for Autolane Direct Delivery. Request autonomous delivery quotes, create autonomous deliveries from a quote, and receive real-time status updates via webhooks.

# Direct Delivery API

The Direct Delivery API enables retailers and DSP aggregators to request autonomous delivery quotes, create autonomous deliveries from those quotes, and receive real-time status updates via webhooks.

The flow is two-step: **quote first, then create**. A quote returns availability, pickup ETA, delivery ETA, and locked delivery cost; the create call promotes that quote into an active delivery, honoring the same locked terms.

## Authentication

All requests require an **Autolane API key** in the `Authorization` header and a per-site `x-retailer-id` header. Your organization and retailer context are resolved automatically from these.

```bash
curl -X POST https://api.goautolane.com/dd/v1/deliveries/quote \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-retailer-id: a1b2c3d4-e5f6-7890-1234-567890abcdef" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

### Permissions

API keys carry granular permissions:

| Permission         | Grants                                                                                                                           |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `deliveries:read`  | List and view deliveries.                                                                                                        |
| `deliveries:write` | Request quotes, create deliveries, and update or cancel them. Required for any DSP-aggregator integration that books deliveries. |
| `webhooks:manage`  | Register, update, and delete webhook endpoints.                                                                                  |

Create API keys in the [Autolane Portal](https://portal.goautolane.com) under **Integrations** > **API Keys**. Toggle **dev mode** in the portal to switch between production and sandbox keys; the dev-mode badge confirms which environment your new key targets. See [Authentication](/getting-started/authentication#direct-delivery-api) for the full walk-through.

## Request fields

### `external_delivery_id`

A retailer-provided string (1 to 255 characters) that uniquely identifies the delivery in your own system. Scoped per-retailer: two different retailers can use the same `external_delivery_id` without collision.

The same value is also the **idempotency key** for the create call. Re-POSTing `POST /dd/v1/deliveries` with an `external_delivery_id` whose quote was already consumed returns `409 QUOTE_ALREADY_CONSUMED` (see [Idempotency](#idempotency) below).

### `customer.phone`

A phone number in **strict E.164 format**: a leading `+`, then 7 to 15 digits, with the first digit between 1 and 9.

```
+15551234567   valid
+442072343456  valid
15551234567    rejected (missing leading +)
+0123456       rejected (leading digit is 0)
```

### `delivery_address`

Required fields: `street`, `city`, `state`, `zip`. Optional `unit` and `location` (`{ lat, lng }`). Provide `location` to skip server-side geocoding; omit it and the API geocodes the address. A `GEOCODE_FAILED` response (422) means geocoding came back empty.

### `ready_by`

ISO 8601 timestamp for when the order will be ready at the store. Must be in the future relative to server clock. Failures return `400 INVALID_READY_BY`.

### `order_items`

An array of up to **50** items. Each item is a JSON object with no required fields and a 4096-byte stringified-JSON cap per item.

```json
{
  "order_items": [
    { "name": "12oz Latte", "quantity": 1, "sku": "BEV-1024", "unit_price_cents": 525 },
    { "name": "Croissant", "quantity": 2 }
  ]
}
```

In v1 the field is free-form to support a wide range of retailer SKU shapes. We strongly recommend including at least:

| Field              | Recommended value                                                                    |
| ------------------ | ------------------------------------------------------------------------------------ |
| `name`             | Short human-readable item name surfaced to the driver during pickup.                 |
| `quantity`         | Integer count.                                                                       |
| `sku`              | Your internal SKU identifier; useful for reconciliation and (future) returns flows.  |
| `unit_price_cents` | Per-unit price in cents. Enables future receipt/refund flows and per-item reporting. |

A future API version will enforce these as required fields; integrations that ship them now will not need to change.

### `pickup_instructions` and `dropoff_instructions`

Optional free text, up to 2000 characters each, accepted on the quote and on `PATCH /dd/v1/deliveries/{external_delivery_id}`. `pickup_instructions` is the store-side note shown to the driver from assignment until the order is loaded, such as a pickup or driver code. `dropoff_instructions` is the customer-side note shown to the driver on the way to and at the customer, such as a gate code or where to leave the order. On `PATCH`, send one or both fields without `status`; `null` or an empty string clears a field, an absent field is left unchanged, and edits are accepted until the delivery reaches `COMPLETED`, `FAILED`, `CANCELLED`, `EXPIRED`, or a return status. Create, list, and get responses return both fields, as does the `PATCH` response after an instructions edit; a cancel response carries only `external_delivery_id` and `status`.

Integrations created before `dropoff_instructions` existed may be in legacy mode, where `pickup_instructions` is treated as the dropoff note and both fields read back the same value; when both are sent, `dropoff_instructions` is used unless it is null or empty. Ask your Autolane contact to switch your account to the current behavior. In legacy mode the two names address one shared note, so clearing works the same way: send either field on its own as `null` or an empty string, or send both as `null`. A body such as `{ "pickup_instructions": "A", "dropoff_instructions": null }` sets the shared note to `A`, because a null `dropoff_instructions` never erases a note sent under `pickup_instructions`.

## Delivery Lifecycle

```mermaid
stateDiagram-v2
    [*] --> SCHEDULED : POST /dd/v1/deliveries (from a valid quote)
    SCHEDULED --> ASSIGNED : autonomous vehicle assigned
    SCHEDULED --> CANCELLED : PATCH status=CANCELLED
    SCHEDULED --> EXPIRED : never dispatched past ready_by + grace period
    ASSIGNED --> EN_ROUTE_TO_STORE
    EN_ROUTE_TO_STORE --> ARRIVED_AT_STORE
    ARRIVED_AT_STORE --> WAITING_FOR_LOAD
    WAITING_FOR_LOAD --> LOADED
    LOADED --> EN_ROUTE_TO_CUSTOMER
    EN_ROUTE_TO_CUSTOMER --> ARRIVED_AT_CUSTOMER
    ARRIVED_AT_CUSTOMER --> COMPLETED
    ASSIGNED --> CANCELLED : PATCH status=CANCELLED (in-flight pickup cancelled too)
    EN_ROUTE_TO_STORE --> CANCELLED
    ARRIVED_AT_STORE --> CANCELLED
    WAITING_FOR_LOAD --> CANCELLED
    LOADED --> FAILED : pickup aborted
    LOADED --> RETURN_IN_PROGRESS : PATCH status=CANCELLED (returns enabled)
    EN_ROUTE_TO_CUSTOMER --> RETURN_IN_PROGRESS
    ARRIVED_AT_CUSTOMER --> RETURN_IN_PROGRESS
    RETURN_IN_PROGRESS --> RETURN_ARRIVED
    RETURN_ARRIVED --> RETURNED
    RETURNED --> [*]
    EN_ROUTE_TO_CUSTOMER --> FAILED
    COMPLETED --> [*]
    CANCELLED --> [*]
    EXPIRED --> [*]
    FAILED --> [*]
```

### One status vocabulary

A delivery's `status` speaks a single vocabulary everywhere it appears — the response to `POST /dd/v1/deliveries`, the top-level `status` on `GET /dd/v1/deliveries` and `GET /dd/v1/deliveries/{external_delivery_id}`, the list endpoint's `?status=` filter, and the `status` carried by `pickup_status.changed` webhooks.

Values: `SCHEDULED`, `ASSIGNED`, `EN_ROUTE_TO_STORE`, `ARRIVED_AT_STORE`, `WAITING_FOR_LOAD`, `LOADED`, `EN_ROUTE_TO_CUSTOMER`, `ARRIVED_AT_CUSTOMER`, `COMPLETED`, `FAILED`, `CANCELLED`, `EXPIRED`, `RETURN_IN_PROGRESS`, `RETURN_ARRIVED`, `RETURNED`.

* A newly created delivery returns `status: "SCHEDULED"`.
* `EXPIRED` is a terminal status for deliveries that were never dispatched before `ready_by` plus the grace period; it never appears in webhooks (no dispatch ever happened).
* The nested `pickup.status` field on GET/list responses carries the same value as the top-level `status` and is retained for response-shape stability — read the top-level field.
* `RETURN_IN_PROGRESS`, `RETURN_ARRIVED`, and `RETURNED` appear only on sites with returns enabled, when a cancel lands after the order is loaded. `RETURNED` is terminal. See [Returns](#returns).

The diagram above traces the full lifecycle.

### Quote Semantics

* A quote is identified by `(retailer, external_delivery_id)`. Re-quoting with the same key replaces the prior quote.
* A quote is valid for **5 minutes**. After `expires_at`, calling `POST /dd/v1/deliveries` returns `410 QUOTE_EXPIRED`.
* A quote can only be consumed once. After a successful create, re-creating with the same `external_delivery_id` returns `409 QUOTE_ALREADY_CONSUMED`.
* A quote with `available: false` cannot be promoted to a delivery. Calling create returns `422 QUOTE_NOT_AVAILABLE`.

### Cancellation

Cancel a delivery via `PATCH /dd/v1/deliveries/{external_delivery_id}` with `{ "status": "CANCELLED" }`. The body may also carry an optional `cancellation_reason`: `CUSTOMER_CANCELLED`, `COLD_CHAIN_EXPIRED`, or `OTHER`. Any other value is recorded as `OTHER`. The reason never decides what happens; the delivery's status does.

What a cancel does depends on where the delivery is:

| Delivery status when the cancel lands              | Sites with returns enabled                                                                                                                                                                                                                                                 | Sites without returns |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `SCHEDULED` through `WAITING_FOR_LOAD`             | `200`, status `CANCELLED`, a `CANCELLED` webhook. A pickup in progress is cancelled with it.                                                                                                                                                                               | same                  |
| `LOADED`, `EN_ROUTE_TO_CUSTOMER`                   | `200`, status `RETURN_IN_PROGRESS`, or `RETURN_ARRIVED` when the vehicle is already back at the store. The order goes back to the store; see [Returns](#returns).                                                                                                          | `409 CONFLICT`        |
| `ARRIVED_AT_CUSTOMER`                              | `200`, status `RETURN_IN_PROGRESS`, while the trunk is still closed. Once the trunk has opened at the customer's door, the handoff wins instead: `409 DELIVERY_ALREADY_COMPLETED` with `current_status: ARRIVED_AT_CUSTOMER` (the handoff is under way, not yet finished). | `409 CONFLICT`        |
| `RETURN_IN_PROGRESS`, `RETURN_ARRIVED`, `RETURNED` | `200` with the current status. No new webhook.                                                                                                                                                                                                                             | same                  |
| `COMPLETED`                                        | `409 DELIVERY_ALREADY_COMPLETED`                                                                                                                                                                                                                                           | `409 CONFLICT`        |
| `CANCELLED`, `EXPIRED`                             | `200` with the current status. No new webhook.                                                                                                                                                                                                                             | `409 CONFLICT`        |
| `FAILED`                                           | `200`, status `CANCELLED`. The cancel is recorded and a `CANCELLED` webhook is sent.                                                                                                                                                                                       | `409 CONFLICT`        |

Every `409` carries `details.current_status` with the delivery's current status. A `200` body is `{ "success": true, "data": { "external_delivery_id": "...", "status": "..." } }`. Returns are enabled per site; ask your Autolane contact to switch them on. The same `PATCH` endpoint edits `pickup_instructions` and `dropoff_instructions` when sent without `status`; see Request fields above.

### Returns

When a site has returns enabled and a cancel arrives at `LOADED` or later, the vehicle brings the order back to the store it was picked up from. The delivery keeps its `external_delivery_id` and reports the return through the same `pickup_status.changed` webhook, with three statuses:

| Status               | Meaning                                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `RETURN_IN_PROGRESS` | The cancel was accepted after load. The vehicle finishes any other drops on the same trip, then heads back to the store. |
| `RETURN_ARRIVED`     | The vehicle is back at the store.                                                                                        |
| `RETURNED`           | The store has taken the order back. Terminal.                                                                            |

Guarantees:

* The order is always `RETURN_IN_PROGRESS`, then `RETURN_ARRIVED`, then `RETURNED`. Return events for one delivery are sent in that order: a later return event waits until the earlier one was delivered to your endpoint or given up on after its retries. This holds even when the cancel response itself carries `RETURN_ARRIVED` directly (vehicle already at the store, see the cancel table above): the `RETURN_IN_PROGRESS` webhook still goes out first.
* After `RETURN_IN_PROGRESS`, the delivery never reports `EN_ROUTE_TO_CUSTOMER`, `ARRIVED_AT_CUSTOMER`, or `COMPLETED`.
* A cancel that starts a return never reports `CANCELLED` or `FAILED`.
* If the handoff to the customer and the cancel race, the handoff wins: the cancel gets `409 DELIVERY_ALREADY_COMPLETED`. The handoff counts as under way once the trunk has opened at the customer's door, even before the delivery reports `COMPLETED`; `details.current_status` is `ARRIVED_AT_CUSTOMER` while the handoff is still under way, or `COMPLETED` once it has finished.
* `stall_id` never changes during a return. The return stall is a separate field, `return_stall_id`.

Fields on the return statuses, on the webhook and on `GET /dd/v1/deliveries/{external_delivery_id}`:

| Field               | Rule                                                                                                                                                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `return_reason`     | Present on the three return statuses: `CUSTOMER_CANCELLED`, `COLD_CHAIN_EXPIRED`, or `OTHER`, normalized from the cancel's `cancellation_reason` (any unrecognized or non-string value becomes `OTHER`), or `null` when the cancel carried none. |
| `return_stall_id`   | Sites with stall reporting: present on the three return statuses, `null` until a return stall is recorded, then a short uppercase string such as `B21`. Sites without stall reporting: absent.                                                   |
| `return_fee_cents`  | Present on `RETURNED`, and on `GET` once the delivery is `RETURNED`: a flat per-site fee in cents, or `null` when no fee is set.                                                                                                                 |
| `estimated_arrival` | Webhook only. On `RETURN_IN_PROGRESS` it is the estimated arrival back at the store, including any other drops on the trip. `null` on `RETURN_ARRIVED` and `RETURNED`.                                                                           |
| `vehicle_location`  | Same present / null / absent rules as every other status, sent through `RETURNED`.                                                                                                                                                               |

Payload examples are on the [webhook page](/direct-delivery/api-reference/webhooks/events-verification#return-fields).

## Idempotency

The Direct Delivery API does not use a separate `Idempotency-Key` header. The `external_delivery_id` you set on the quote and create calls **is** the idempotency key:

* `POST /dd/v1/deliveries/quote` with the same `external_delivery_id` replaces the prior quote in place (race-safe via Postgres `ON CONFLICT`); the response always reflects the latest quote.
* `POST /dd/v1/deliveries` with an `external_delivery_id` whose quote was already consumed returns `409 QUOTE_ALREADY_CONSUMED`; the response is final and safe to surface to the caller without retrying.

Webhook delivery has its own idempotency contract; see [Idempotency](/direct-delivery/api-reference/webhooks/events-verification#idempotency) on the webhook page.

## Error Codes

| HTTP | Code                         | When                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ---- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `VALIDATION_ERROR`           | Request body fails Zod validation. `details` carries per-field messages.                                                                                                                                                                                                                                                                                                                                                                    |
| 400  | `INVALID_READY_BY`           | `ready_by` is in the past.                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 400  | `MISSING_RETAILER_ID`        | `x-retailer-id` header is missing.                                                                                                                                                                                                                                                                                                                                                                                                          |
| 400  | `INVALID_RETAILER_ID`        | `x-retailer-id` is not a valid UUID.                                                                                                                                                                                                                                                                                                                                                                                                        |
| 401  | `INVALID_API_KEY`            | Bearer token missing or invalid.                                                                                                                                                                                                                                                                                                                                                                                                            |
| 403  | `PERMISSION_DENIED`          | API key lacks the required `deliveries:*` or `webhooks:*` scope.                                                                                                                                                                                                                                                                                                                                                                            |
| 403  | `WRONG_ENV_KEY`              | Production key used against sandbox host or vice versa.                                                                                                                                                                                                                                                                                                                                                                                     |
| 403  | `ORG_NOT_MAPPED`             | API key's organization isn't mapped to a brand in Autolane.                                                                                                                                                                                                                                                                                                                                                                                 |
| 403  | `RETAILER_NOT_AUTHORIZED`    | The site identified by `x-retailer-id` isn't authorized for this organization.                                                                                                                                                                                                                                                                                                                                                              |
| 403  | `RETAILER_INACTIVE`          | The retailer resolved from `x-retailer-id` is not active.                                                                                                                                                                                                                                                                                                                                                                                   |
| 404  | `QUOTE_NOT_FOUND`            | Create-delivery called for an `external_delivery_id` with no quote.                                                                                                                                                                                                                                                                                                                                                                         |
| 404  | `NOT_FOUND`                  | `GET` or `PATCH` for a delivery that doesn't exist.                                                                                                                                                                                                                                                                                                                                                                                         |
| 409  | `QUOTE_ALREADY_CONSUMED`     | Create-delivery called for a quote that has already been consumed.                                                                                                                                                                                                                                                                                                                                                                          |
| 409  | `DELIVERY_ALREADY_COMPLETED` | Cancel called for a delivery already handed to the customer (`details.current_status` is `COMPLETED`), or one where the trunk has opened at the customer's door and the handoff is under way (`details.current_status` is `ARRIVED_AT_CUSTOMER`).                                                                                                                                                                                           |
| 409  | `CONFLICT`                   | Cancel called outside the cancellable window on a site without returns; an instructions edit while the delivery is terminal or a return is in progress (return statuses are not terminal, but still block an edit); or a cancel on a delivery whose order is independently `CANCELLED`/`EXPIRED` while its pickup is still live, which answers `CONFLICT` even on a site with returns enabled. `details.current_status` carries the status. |
| 410  | `QUOTE_EXPIRED`              | Create-delivery called for a quote past its `expires_at`.                                                                                                                                                                                                                                                                                                                                                                                   |
| 422  | `QUOTE_NOT_AVAILABLE`        | Create-delivery called for a quote where `available: false`.                                                                                                                                                                                                                                                                                                                                                                                |
| 422  | `GEOCODE_FAILED`             | Quote address could not be geocoded and no `location` was provided.                                                                                                                                                                                                                                                                                                                                                                         |
| 500  | `INTERNAL_ERROR`             | Unexpected server error. Retry-safe for idempotent operations; please report repeats.                                                                                                                                                                                                                                                                                                                                                       |
| 503  | `RETAILER_NOT_CONFIGURED`    | Retailer is missing service-area or `delivery_price_cents` configuration.                                                                                                                                                                                                                                                                                                                                                                   |