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

# Changelog

## Returns after load

A cancel that arrives after the order is loaded can now bring the order back
to the store instead of being refused. Returns are enabled per site; ask your
Autolane contact to switch them on.

## What changed

* **`PATCH /dd/v1/deliveries/{external_delivery_id}`** with `status: CANCELLED`
  at `LOADED`, `EN_ROUTE_TO_CUSTOMER`, or `ARRIVED_AT_CUSTOMER` answers `200`
  with `status: RETURN_IN_PROGRESS` on sites with returns enabled, unless the
  trunk has already opened at the customer's door, in which case the handoff
  wins instead. Sites without returns still get `409`. The body accepts an
  optional `cancellation_reason`; any value is accepted, and an unrecognized
  or non-string value is recorded as `OTHER`.
* **Three new statuses**, `RETURN_IN_PROGRESS`, `RETURN_ARRIVED`, `RETURNED`,
  on `pickup_status.changed` and on `GET`. They always arrive in that order and
  `RETURNED` is terminal. After `RETURN_IN_PROGRESS` a delivery never reports
  `EN_ROUTE_TO_CUSTOMER`, `ARRIVED_AT_CUSTOMER`, or `COMPLETED`.
* **New fields** on the return statuses: `return_reason`, `return_stall_id`
  (sites with stall reporting), and `return_fee_cents` on `RETURNED`.
  `estimated_arrival` on `RETURN_IN_PROGRESS` is the arrival back at the store.
* **`409 DELIVERY_ALREADY_COMPLETED`** when a cancel lands after the handoff to
  the customer, or once the trunk has opened at the customer's door.
* A repeat cancel during or after a return answers `200` with the current
  status and sends no new webhook.
* **Sandbox.** The portal's Advance button steps a returning delivery through
  `RETURN_ARRIVED` (with an optional Return stall ID) and `RETURNED`.

## Migration

No action is required unless you want returns. Integrations that reject a
status they do not know should add the three return statuses. Integrations that
de-duplicate webhook events on `(external_delivery_id, status)` should exempt
events whose `previous_status` equals `status`, as for stall corrections.

## Cancel accepts cleared instruction fields

A cancel through `PATCH /dd/v1/deliveries/{external_delivery_id}` no longer
fails when the body also carries an instruction field set to `null` or an
empty string.

## What changed

* **`PATCH /dd/v1/deliveries/{external_delivery_id}`** cancels the delivery
  when `status` is `CANCELLED` and any `pickup_instructions` or
  `dropoff_instructions` key present is `null` or an empty string. Those keys
  are ignored; a cancel never edits instructions.
* An instruction field with any value other than `null` or an empty string
  next to `status` is still refused with `400`, and `status` still accepts
  only `CANCELLED`.

## Migration

No action is required. Clients that send `null` for every empty note can
cancel without stripping those keys first.

## Pickup and dropoff instructions

Deliveries now carry two notes: `pickup_instructions` for the store side and
`dropoff_instructions` for the customer side.

## What changed

* **`POST /dd/v1/deliveries/quote`** accepts `dropoff_instructions` beside
  `pickup_instructions`. Both are optional, nullable, and up to 2000
  characters.
* **`PATCH /dd/v1/deliveries/{external_delivery_id}`** edits either note. Send
  one or both fields without `status`; `null` or an empty string clears a
  field, and an absent field is left unchanged. Edits are accepted until the
  delivery reaches `COMPLETED`, `FAILED`, `CANCELLED`, or `EXPIRED`. A cancel
  still sends `status` on its own.
* **`POST /dd/v1/deliveries`**, **`GET /dd/v1/deliveries`**, and
  **`GET /dd/v1/deliveries/{external_delivery_id}`** return both fields on
  every delivery. The `PATCH` response returns them after an instructions
  edit.
* `pickup_instructions` is shown to the driver from assignment until the order
  is loaded. `dropoff_instructions` is shown on the way to and at the
  customer.

## Migration

No action is required. 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. In that mode a body
that sends both fields uses `dropoff_instructions` unless it is null or empty,
and clearing either field clears the shared note. Ask your Autolane contact to
switch your account to the current behavior.

## Stall ID on store-arrival webhooks

Sites can now report the stall the vehicle is parked in at the store. Ask your
Autolane contact to enable **stall reporting** for a site.

## What changed

* **`pickup_status.changed`** carries a new `data.stall_id` field on
  `ARRIVED_AT_STORE`, `WAITING_FOR_LOAD`, and `LOADED` for sites with stall
  reporting enabled. It is `null` when no stall was recorded for the arrival
  and is omitted on every other status and for every other site.
* **Corrections.** If the stall changes while the delivery is at
  `ARRIVED_AT_STORE` or `WAITING_FOR_LOAD`, you receive another
  `pickup_status.changed` with `previous_status` equal to `status` and the new
  `stall_id`. The newest event by `timestamp` wins. A delivery that is already
  `LOADED` gets no correction event.
* **`GET /dd/v1/deliveries/{external_delivery_id}`** returns `stall_id`
  under the same rule.
* **Sandbox.** The portal's Advance button offers an optional Stall ID when
  stepping into `ARRIVED_AT_STORE`, so you can exercise the populated shape.

## Migration

No action is required unless you want the feature. To enable stall reporting
for a site, contact your Autolane representative. Integrations that
de-duplicate webhook events on `(external_delivery_id, status)` should exempt
events whose `previous_status` equals `status`.

## Delivery status now reports the delivery lifecycle

The top-level `status` field on delivery responses now reports the
**delivery lifecycle** — the same vocabulary emitted by
`pickup_status.changed` webhooks — instead of the internal order state
machine. This resolves a long-standing inconsistency where the GET
endpoint could report `PENDING` while webhooks were progressing through
`ASSIGNED`, `EN_ROUTE_TO_STORE`, and beyond.

## What changed

* **`POST /dd/v1/deliveries`** now returns `status: "SCHEDULED"` for a
  newly created delivery (previously `"PENDING"`).
* **`GET /dd/v1/deliveries`** and
  **`GET /dd/v1/deliveries/{external_delivery_id}`** now return the
  delivery lifecycle status at the top level: `SCHEDULED`, `ASSIGNED`,
  `EN_ROUTE_TO_STORE`, `ARRIVED_AT_STORE`, `WAITING_FOR_LOAD`, `LOADED`,
  `EN_ROUTE_TO_CUSTOMER`, `ARRIVED_AT_CUSTOMER`, `COMPLETED`, `FAILED`,
  `CANCELLED`, or `EXPIRED`. The GET response and the webhook stream now
  always agree.
* **The list endpoint's `?status=` filter** accepts the same lifecycle
  values. The legacy order-status vocabulary (`PENDING`, `READY`,
  `ACCEPTED`, `dispatching`, `en_route`, `arriving`, `arrived`,
  `waiting_for_pickup`, `returning`) is no longer accepted and returns
  `400 VALIDATION_ERROR`.
* **`estimated_arrival`** in `pickup_status.changed` webhook payloads is
  now populated with the delivery ETA (estimated arrival at the
  customer, locked at quote time). Previously this field was always
  `null`.
* **Sandbox `vehicle_location`** is now populated: the synthetic vehicle
  records a location fix at every portal advance, so status-change
  payloads for subscribers of `pickup.vehicle_location.changed` carry a
  fresh position instead of `null`.

## Migration

If your integration reads the top-level `status` or filters the list
endpoint by status, switch to the lifecycle vocabulary:

| Before (order status)    | After (delivery lifecycle)                                  |
| ------------------------ | ----------------------------------------------------------- |
| `PENDING`                | `SCHEDULED`                                                 |
| `READY`                  | `SCHEDULED` (until dispatch) or the current dispatch state  |
| `ACCEPTED` … `returning` | The corresponding `ASSIGNED` … `ARRIVED_AT_CUSTOMER` states |
| `CANCELLED`              | `CANCELLED` (unchanged)                                     |
| `EXPIRED`                | `EXPIRED` (unchanged)                                       |

The nested `pickup.status` field is unchanged and now always matches the
top-level `status`; prefer reading the top-level field.

## API endpoint paths moved to /dd/v1/*

The Direct Delivery API endpoints are now served under the `/dd/v1/*`
prefix instead of `/api/retailer/*`. This is purely a URL change.
Request bodies, response shapes, headers, and authentication are
unchanged.

## What changed

The public API surface is now organized around versioned product
prefixes (`/dd/v1/*` for Direct Delivery v1). The old `/api/retailer/*`
paths are no longer reachable from `api.goautolane.com`.

## Migration

If you were calling endpoints at `/api/retailer/*`, switch to `/dd/v1/*`:

| Before                                                  | After                                            |
| ------------------------------------------------------- | ------------------------------------------------ |
| `POST /api/retailer/deliveries/quote`                   | `POST /dd/v1/deliveries/quote`                   |
| `POST /api/retailer/deliveries`                         | `POST /dd/v1/deliveries`                         |
| `GET /api/retailer/deliveries`                          | `GET /dd/v1/deliveries`                          |
| `GET /api/retailer/deliveries/{external_delivery_id}`   | `GET /dd/v1/deliveries/{external_delivery_id}`   |
| `PATCH /api/retailer/deliveries/{external_delivery_id}` | `PATCH /dd/v1/deliveries/{external_delivery_id}` |
| `POST /api/retailer/webhooks`                           | `POST /dd/v1/webhooks`                           |
| `GET /api/retailer/webhooks`                            | `GET /dd/v1/webhooks`                            |
| `PUT /api/retailer/webhooks`                            | `PUT /dd/v1/webhooks`                            |
| `DELETE /api/retailer/webhooks`                         | `DELETE /dd/v1/webhooks`                         |

If you're using the generated SDK, regenerate it from the latest
OpenAPI spec. No code changes required.

The base URL (`https://api.goautolane.com`), authentication
(`Authorization: Bearer <api_key>`), and the `x-retailer-id` header
remain unchanged.

## v1.0.0 Direct Delivery API GA

The Direct Delivery API is generally available. Retailers can now request
quotes, submit deliveries, and subscribe to real-time status updates.

## Added

### Deliveries

* `POST /api/retailer/deliveries/quote`: request a delivery quote with
  pickup ETA, delivery ETA, and a locked cost. Quotes are valid for 5 minutes.
* `POST /api/retailer/deliveries`: consume a quote to create a delivery.
* `GET /api/retailer/deliveries`: list deliveries for your retailer.
* `GET /api/retailer/deliveries/{external_delivery_id}`: fetch a single
  delivery by your external ID.
* `PATCH /api/retailer/deliveries/{external_delivery_id}`: update a
  delivery (cancellation supported in v1).

### Webhooks

* `POST /api/retailer/webhooks`: register a webhook URL. The signing
  secret is returned only once on creation.
* `GET /api/retailer/webhooks`: view current configuration.
* `PUT /api/retailer/webhooks`: update the URL or subscribed events.
* `DELETE /api/retailer/webhooks`: remove the subscription.

Supported events:

* `pickup_status.changed`: fires on each pickup status transition:
  `ASSIGNED`, `EN_ROUTE_TO_STORE`, `ARRIVED_AT_STORE`, `WAITING_FOR_LOAD`,
  `LOADED`, `EN_ROUTE_TO_CUSTOMER`, `ARRIVED_AT_CUSTOMER`, `COMPLETED`,
  `FAILED`, `CANCELLED`.
* `tracking_link.created`: fires when a customer-facing tracking link
  is generated for a delivery.

### Authentication

* Bearer authentication using an API key generated in the Autolane
  Portal, scoped to your organization with granular permissions.
* `x-retailer-id` header required on every request, set to the site
  UUID from the Autolane Portal.