Skip to navigation

Direct Delivery

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.

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:

PermissionGrants
deliveries:readList and view deliveries.
deliveries:writeRequest quotes, create deliveries, and update or cancel them. Required for any DSP-aggregator integration that books deliveries.
webhooks:manageRegister, update, and delete webhook endpoints.

Create API keys in the Autolane Portal 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 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 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.

{
"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:

FieldRecommended value
nameShort human-readable item name surfaced to the driver during pickup.
quantityInteger count.
skuYour internal SKU identifier; useful for reconciliation and (future) returns flows.
unit_price_centsPer-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

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

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.

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 landsSites with returns enabledSites without returns
SCHEDULED through WAITING_FOR_LOAD200, status CANCELLED, a CANCELLED webhook. A pickup in progress is cancelled with it.same
LOADED, EN_ROUTE_TO_CUSTOMER200, 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.409 CONFLICT
ARRIVED_AT_CUSTOMER200, 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, RETURNED200 with the current status. No new webhook.same
COMPLETED409 DELIVERY_ALREADY_COMPLETED409 CONFLICT
CANCELLED, EXPIRED200 with the current status. No new webhook.409 CONFLICT
FAILED200, 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:

StatusMeaning
RETURN_IN_PROGRESSThe cancel was accepted after load. The vehicle finishes any other drops on the same trip, then heads back to the store.
RETURN_ARRIVEDThe vehicle is back at the store.
RETURNEDThe 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}:

FieldRule
return_reasonPresent 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_idSites 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_centsPresent 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_arrivalWebhook 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_locationSame present / null / absent rules as every other status, sent through RETURNED.

Payload examples are on the webhook page.

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 on the webhook page.

Error Codes

HTTPCodeWhen
400VALIDATION_ERRORRequest body fails Zod validation. details carries per-field messages.
400INVALID_READY_BYready_by is in the past.
400MISSING_RETAILER_IDx-retailer-id header is missing.
400INVALID_RETAILER_IDx-retailer-id is not a valid UUID.
401INVALID_API_KEYBearer token missing or invalid.
403PERMISSION_DENIEDAPI key lacks the required deliveries:* or webhooks:* scope.
403WRONG_ENV_KEYProduction key used against sandbox host or vice versa.
403ORG_NOT_MAPPEDAPI key’s organization isn’t mapped to a brand in Autolane.
403RETAILER_NOT_AUTHORIZEDThe site identified by x-retailer-id isn’t authorized for this organization.
403RETAILER_INACTIVEThe retailer resolved from x-retailer-id is not active.
404QUOTE_NOT_FOUNDCreate-delivery called for an external_delivery_id with no quote.
404NOT_FOUNDGET or PATCH for a delivery that doesn’t exist.
409QUOTE_ALREADY_CONSUMEDCreate-delivery called for a quote that has already been consumed.
409DELIVERY_ALREADY_COMPLETEDCancel 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).
409CONFLICTCancel 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.
410QUOTE_EXPIREDCreate-delivery called for a quote past its expires_at.
422QUOTE_NOT_AVAILABLECreate-delivery called for a quote where available: false.
422GEOCODE_FAILEDQuote address could not be geocoded and no location was provided.
500INTERNAL_ERRORUnexpected server error. Retry-safe for idempotent operations; please report repeats.
503RETAILER_NOT_CONFIGUREDRetailer is missing service-area or delivery_price_cents configuration.