> ## Documentation Index
> Fetch the complete documentation index at: https://docs.listo.delivery/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Notes on the Listo Partner Delivery API:
> - Every endpoint is under `https://api.listo.delivery/v1/` and authenticates with a bearer API key.
> - This documentation site is at https://docs.listo.delivery. The full machine-readable reference is the OpenAPI document at https://api.listo.delivery/docs/openapi.yaml (served by the API host, not by this site).
> - Any page of this site is also available as raw markdown by appending `.md` to its URL, for example https://docs.listo.delivery/en/guides/webhooks.md. The index of every page is https://docs.listo.delivery/llms.txt and all of them in one file is https://docs.listo.delivery/llms-full.txt.
> - Every page is published in English under https://docs.listo.delivery/en/ and in Spanish under https://docs.listo.delivery/es/, with the same path after the language. The API reference is under https://docs.listo.delivery/api-reference/ in English and https://docs.listo.delivery/es/api-reference/ in Spanish.
> - To debug an integration yourself: `GET /v1/request_logs` lists the calls your keys made with what you sent and what was answered, `GET /v1/events` lists the events Listo emitted, `GET /v1/webhook_deliveries` lists every attempt to deliver them, `POST /v1/events/{id}/resend` sends one event again and `POST /v1/test_webhooks` sends a sample event (`"test": true`). The page https://docs.listo.delivery/en/guides/debugging describes them. A resent event is a snapshot from when it happened; `GET /v1/deliveries` is still how you recover current state.
> - Every error response has `error.doc_url`, a page that explains that error code and what to do, and `error.request_id`, the same value as the `X-Request-Id` response header. All the pages are listed at https://docs.listo.delivery/en/errors. Filter `GET /v1/request_logs` with `request_id` to read the logged call.
> - A `409 account_exists` from `POST /v1/stores` means the restaurant already has its own Listo account: connect it instead of creating it, as the page https://docs.listo.delivery/en/guides/connect-existing-account describes.
> - Every live store carries one Plan Listo, charged to the partner. The exception is a connected restaurant that had already paid its current month: it keeps paying its own plan until its next renewal, shown as `paid_by: "store"` with the handover date in `pending_paid_by_on`. A `402 plan_required` from a quote, a delivery create or a retry means the store's plan is not paid or has ended; the page https://docs.listo.delivery/en/guides/plan-and-billing describes the plan, the `plan` block on a store and how to end or restart it.
> - An AI assistant (Claude Code, Cursor, Codex) can act through the same API key at `https://api.listo.delivery/mcp`, an MCP server over Streamable HTTP authenticated with `Authorization: Bearer <API key>`. Every tool runs the matching `/v1/` endpoint, and each one that moves money first previews and then needs a confirmation token; the page https://docs.listo.delivery/en/guides/connect-ai-client lists the tools and describes the two steps.

# Quote and create a delivery

> Price a delivery, then turn the quote into a real one.

Dispatching an order is two calls. `POST /v1/quotes` asks what a delivery to a
given address costs and holds that price for you; `POST /v1/deliveries` sends
the quote back and charges the order the price you were shown.

The quote is optional. Take it when a human is about to see the number and
decide something, skip it when nobody is: a create with no `quote_id` prices
itself inline at the current price, in one call.

```ruby quote_and_create.rb theme={null}
# Prices a delivery, then dispatches it at the price it was quoted.
#
#   LISTO_API_KEY=lst_test_... LISTO_STORE_ID=st_... ruby quote_and_create.rb
#
require "json"
require "net/http"
require "uri"

BASE_URL = ENV.fetch("LISTO_API_BASE_URL", "https://api.listo.delivery")
API_KEY = ENV.fetch("LISTO_API_KEY")
STORE_ID = ENV.fetch("LISTO_STORE_ID")

# Your own reference for this order. It is what you will recognise the delivery
# by in every response and every webhook, so use the id your system already has.
ORDER_ID = "order-841"

# The refusal envelope every Listo error carries, or a stand-in built from the
# status when the body is not that envelope at all. Something between you and
# Listo, a proxy or a load balancer, can answer with an HTML error page or with
# JSON of some other shape, and the code that handles a failure must not itself
# fail trying to read it.
def parse_error(response)
  stand_in = {"code" => "HTTP #{response.code}", "param" => nil}
  body = JSON.parse(response.body.to_s)
  error = body["error"] if body.is_a?(Hash)
  error.is_a?(Hash) ? error : stand_in
rescue JSON::ParserError
  stand_in
end

def post(path, payload, headers = {})
  uri = URI("#{BASE_URL}#{path}")

  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"
  request["Content-Type"] = "application/json"
  headers.each { |name, value| request[name] = value }
  request.body = JSON.generate(payload)

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
    http.request(request)
  end

  # Check the status BEFORE parsing the body. Every refusal from Listo answers
  # with the same envelope, so branch on `error.code`, never on the message, and
  # retry only when `error.retryable` says you may.
  unless response.code.start_with?("2")
    abort("POST #{path} refused: #{parse_error(response)["code"]}")
  end

  JSON.parse(response.body)
end

dropoff = {
  "name" => "Ana Torres",
  "phone" => "+528112345678",
  "street" => "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116",
  "lat" => 20.709395,
  "lng" => -103.413393
}

# 1. Ask what the delivery costs. The price is held until `expires_at`, so you
# can show it to your customer before anyone commits to anything. No money
# moves and no courier is called.
quote = post(
  "/v1/quotes",
  {
    "store_id" => STORE_ID,
    "payment_method" => "cash",
    "order_value_cents" => 19_600,
    "dropoff" => dropoff
  }
)

puts "fee #{quote["fee_cents"]} #{quote["currency"]}, of which you pay #{quote["business_fee_cents"]}"
puts "the courier collects #{quote["cash_to_collect_cents"]} at the door"

# 2. Dispatch it at exactly that price by sending the quote id back.
#
# The Idempotency-Key is REQUIRED. Send the same one again after a timeout and
# you get the original delivery back instead of a second courier; send it with
# a different body and the call is refused with `idempotency_conflict`. Key it
# on your order, not on the attempt.
delivery = post(
  "/v1/deliveries",
  {
    "store_id" => STORE_ID,
    "quote_id" => quote["id"],
    "external_id" => ORDER_ID,
    "payment_method" => "cash",
    "order_value_cents" => 19_600,
    "dropoff" => dropoff
  },
  {"Idempotency-Key" => "create-#{ORDER_ID}"}
)

puts "delivery #{delivery["id"]} is #{delivery["status"]}"
puts "follow it at #{delivery["tracking_url"] || "(no link until a courier is assigned)"}"
```

## The quote

A quote sets the price at the moment you ask for it, and holds it for you to
bind. It is not a reservation of a courier. Nothing is dispatched, no money
moves, and letting one expire costs you nothing.

Bind a quote and the order is charged the `fee_cents` and the split you were
shown. The courier network books the trip when the delivery is created, and the
price it books can differ slightly from the one quoted. We book the very quote
you were shown whenever it is still valid, so this is rare. When it happens, the
difference appears on your ledger as its own `requote_adjustment` line (negative
when the booked trip cost more, positive when it cost less). It never changes the
`fee_cents`, the split or the total your customer sees.

You send the store, the dropoff, and enough about the order to price it: the
payment method and the order value both change what a delivery costs, so a
quote taken without them is a quote for a different order. It answers with:

| Field | What it is |
| - | - |
| `id` | The `qt_...` you send back on the create. |
| `fee_cents` | The whole delivery fee. |
| `business_fee_cents`, `customer_fee_cents` | How that fee splits between you and the customer. The two always sum to `fee_cents`. |
| `cash_to_collect_cents` | On a cash quote, what the courier will collect at the door. `null` otherwise. |
| `vehicle` | `moto` or `car`: which trip `fee_cents` is for. See [sending it by car](#sending-it-by-car). |
| `pickup_duration_min` | Roughly how long until a courier reaches the store. |
| `dropoff_eta_at` | When the order would arrive, as an absolute timestamp. |
| `expires_at` | When the price stops being honoured. |

`expires_at` is at most fifteen minutes out and is often sooner. Two rules
follow from that, and both are refusals rather than surprises:

* Creating against an expired quote returns `400 quote_expired`. Ask for a new
  one and try again; nothing was created.
* A quote is single use. Spending it twice returns `400 quote_consumed`, which
  is what a duplicate submit looks like from here.

> [!WARNING]
>
> **The create has to agree with the quote**
>
> Binding a quote means the price is settled, so the create may not contradict the
> things that price was computed from. `payment_method`, `order_value_cents`,
> `business_fee_share`, `business_fee_cap_cents`, the dropoff `street` and the
> dropoff `apt_unit` must all match the quote, or the create is refused with
> `validation_failed` naming the field that disagrees. Change any of them and you
> need a new quote.
>
> The vehicle has to match too, and it is compared as a whole trip rather than as
> the `vehicle` field alone: a create whose manifest has an item sized `xlarge` is
> a car trip whatever `vehicle` says, so binding it to a quote priced as a moto is
> refused, naming `manifest.items`. Leave `vehicle` out and the create inherits
> the quote's.
>
> The message on that refusal says why and what to do: when the split is what
> disagrees, it tells you to send the same split or request a new quote.
>
> Leaving one out is not a contradiction: an omitted field inherits what the quote
> was priced with. Sending `business_fee_cap_cents` on a create that binds a quote
> taken *without* a ceiling is a mismatch rather than an addition, so put the
> ceiling on the quote too.
>
> `tip_cents` is the exception, and the only one: quotes have no tip field, a tip
> never changes a fee, and any tip you send on a create is compatible with any
> quote you bind.

## The create

Required on every create: `store_id`, `external_id`, `payment_method`,
`order_value_cents`, and a `dropoff` with at least a `name`, a `phone` and a
`street`. Everything else has a default.

`order_value_cents` is the value of the food or goods, and it is yours to state.
It is never summed from the manifest, and it never includes the delivery fee.

### `Idempotency-Key` is required

Send an `Idempotency-Key` header, at most 255 characters, on every create. It is
not optional, and a create without one is refused.

Key it on your order rather than on the attempt. That is what makes the header
worth having: when a create times out you do not know whether a courier was
dispatched, and repeating it with the same key answers with the original
delivery instead of sending a second one. The original response is replayed for
24 hours, status code included.

Reusing a key with a *different* body returns `409 idempotency_conflict`. That
is deliberate: it means two different orders were sent under one key, and
answering with either of them would be wrong.

A repeat that arrives while the first request with that key is still being
processed also returns `409 idempotency_conflict`. It created nothing, and it
does not mean the first request failed: that request goes on to its own answer,
and once it has answered, the same request with the same key replays it.

### `external_id` is how you will recognise it

`external_id` is your own reference for the order. It comes back on every
response and every webhook, it is queryable with
`GET /v1/deliveries?external_id=...`, and it is unique per key mode, so your
staging system and your production system can both use `order-841` without ever
colliding. Sending one you already used in that mode is refused.

You do not have to store Listo's `dlv_...` id at all if you would rather not.

### Payment methods

| `payment_method` | Who collects the money | Where the delivery starts |
| - | - | - |
| `prepaid` | You already did, through your own checkout. Listo does not charge your customer. | `dispatching`, already `paid` |
| `cash` | The courier, at the door. See [cash at the door](https://docs.listo.delivery/en/concepts/cash-at-the-door.md). | `dispatching`, and `payment.status` stays `unpaid` |
| `card` | Listo, through a payment link that Listo sends your customer. | `awaiting_payment` until they pay, then it dispatches itself |

On `card` the response carries `payment.url`: the branded link Listo sends your
customer itself, as soon as the order is created (see
[what Listo sends your customer](#what-listo-sends-your-customer)). You can show
it on your own screen or keep it, but you do not have to send it, and you should
not: your customer would get the same link twice, from two senders. The delivery
sits there until it is paid, then dispatches on its own. The link disappears
from the object the moment it is paid or cancelled, because a dead link in a
payload is worse than none, and Listo sends nothing for a link that is no longer
live.

On `prepaid` the whole fee is yours: there is nobody for Listo to collect a
customer share from on an order it never charges the customer of. So the share is
always 100 and `business_fee_cap_cents` is ignored, and how that shows depends on
the endpoint:

* `POST /v1/quotes` with `prepaid` refuses a `business_fee_share` other than 100
  with `422 validation_failed` naming `business_fee_share`. Send 100 or leave it
  out: with no split fields the quote is priced at 100, whatever your store's
  default split is. A quote that names no `payment_method` is priced exactly the
  same way, because the only delivery that can bind it is a `prepaid` one: it
  refuses a share other than 100 too, with a message that says so and that you
  can name the `payment_method` instead. To split the fee with your customer,
  quote as `cash` or `card`.
* `POST /v1/deliveries` with `prepaid` and no `quote_id` forces the share to 100
  and creates the order.
* `POST /v1/deliveries` with `prepaid` and a `quote_id` needs a quote that was
  requested with `payment_method: prepaid` or with no `payment_method`, either of
  which is always priced at 100. Any other quote is refused with
  `422 validation_failed`, naming the first thing that does not match. A quote
  requested as `cash` or `card` is refused naming `payment_method`. A quote whose
  split is not 100 with no ceiling (one saved before this rule) is refused naming
  `business_fee_share` (or `business_fee_cap_cents`), with a message that says to
  request a new quote as `prepaid`.

See [the money model](https://docs.listo.delivery/en/concepts/money.md).

### What Listo sends your customer

Listo talks to the customer of your orders itself, from Listo's own WhatsApp
number and in Spanish, so there is nothing for you to build to keep them
informed. Two things go out:

* **The payment link, on `card` orders.** The moment a `card` order is created
  and waits for payment, Listo sends the customer a WhatsApp with their first
  name, your store's name, what they owe and the `payment.url` link, and says
  the courier leaves as soon as they pay. If that WhatsApp does not reach them
  (the number is not on WhatsApp, or the message fails), Listo sends the same
  link by SMS instead, from a short code, so they still get it. It is sent once
  per order, with nothing for you to call. A link that is no longer live, because
  the order was paid or cancelled, is not sent.
* **The courier's messages, on every order.** Once a courier is assigned, when
  they are on the way, when they are arriving and when the order is delivered,
  Listo sends the customer a WhatsApp from the same number, with the tracking
  link and, if you asked for one, the drop-off code. This includes `prepaid` and
  `cash` orders: paying in your own checkout does not stop Listo from telling the
  customer where their order is.

Send nothing of your own for either of these, or the customer hears it twice.
Everything else you tell them, your order confirmation or your receipt, stays
yours. Nothing about your balance, the fee split or what a delivery costs you
is in any of Listo's messages.

### Tipping the courier

`tip_cents` adds a tip for the courier, in cents, on top of everything else on
the order. It reaches the courier in full: Listo adds nothing to it and keeps
nothing from it.

It is optional, defaults to `0`, and is capped at `100000` (MX\$1,000). Anything
above that, or below zero, is refused with `validation_failed` naming
`tip_cents` before the order is priced, so an impossible tip costs you nothing.

A tip is not part of `fee_cents` and it is not split with your customer. Your
share of the fee and the tip come off your balance together as one movement when
the delivery is created, and if the delivery never happens they come back
together too: a `canceled`, `returned` or `no_courier` order refunds the tip
along with your fee share.

> [!WARNING]
>
> **Tips are for prepaid orders only**
>
> `tip_cents` is accepted on `payment_method: prepaid`. A tip above zero on a
> `cash` or a `card` order is refused with `validation_failed` naming `tip_cents`.
>
> Those two orders are ones Listo collects money for, and a tip declared here
> would be the wrong shape in both. On a cash order the courier is tipped hand to
> hand at the door, by the person who is already handing them money, and a
> declared tip would change what the courier has to collect and count against the
> cash limit. On a card order your customer pays through a Listo link, so a tip
> you fund on top of a charge they paid is a second, different tip. Prepaid is the
> one method where you have already settled with your customer yourself, so the
> tip is unambiguously yours to send.

A tip is decided once, at create, and cannot be raised afterwards. There is no
call that adds a tip to a delivery already on the road.

### Scheduling

Leave `pickup_ready_at` out and the courier is called immediately. Send it and
the order is scheduled: the courier is arranged now, for a pickup then.

It has to be at least 10 minutes out and at most 29 days out. Ten minutes is low
on purpose, because "food ready in fifteen" is the normal case for a machine,
not an edge one.

A scheduled order can be moved with `PATCH /v1/deliveries/{id}` while it is
still `scheduled`, including to now, which dispatches it. Once it has left
`scheduled` the window is fixed and the call returns `409 not_reschedulable`.

### Sending it by car

`vehicle` is `moto`, the default, or `car`. Send `car` for an order that does
not fit on a motorcycle.

* A car is a sedan. We can guarantee a car, never a van.
* There are fewer cars than motorcycles, so a car can take longer to assign.
* A car adds a surcharge to the delivery fee. It is inside `fee_cents` and
  split like the rest of the fee, so the price you are quoted is the whole
  price, and there is no separate line for it.

Each item in `manifest.items` can also carry a `size`: `small` (the default),
`medium`, `large` or `xlarge`. An item sized `xlarge` makes the trip a car even
when `vehicle` says `moto`, because it will not fit on a motorcycle. No other
size does, and neither does the number of items: two `large` items are still a
motorcycle trip. Send the quote the same `vehicle` and the same manifest you
will create with, so the price you show is the price of the trip you book.

Both the quote and the delivery answer with `vehicle`: what the trip was priced
and dispatched as, whatever made it a car. It is not the vehicle that turns up.
That is `courier.vehicle_type` on the delivery, which can occasionally be a car
on a `moto` trip without the fee changing.

A cart too large for one courier trip is refused with `422 order_too_large`,
naming `manifest.items`, before anything is priced or dispatched. Split it into
two deliveries.

### The optional detail

* `manifest.items` is a list of `{name, quantity, size}` for the courier to
  check against. It is descriptive: money comes from `order_value_cents`, and
  the one thing an item can change is the vehicle, through its `size` (see
  [sending it by car](#sending-it-by-car)).
* `handoff` is `meet_at_door` (the default), `leave_at_door` or `meet_outside`.
  Cash orders are always handed over in person.
* `dropoff.notes` are per-order instructions for this address, at most 280
  characters. Standing instructions for the *store* belong on the store as
  `pickup_notes`.
* `verification.dropoff_pincode: true` asks for a code the customer has to give
  the courier. It comes back on the delivery as `verification.pincode` once a
  courier has one. Listo also gives it to your customer in its own courier
  messages, so you only need to show it yourself if you want it on your screen.
* `dropoff.lat` and `dropoff.lng` are optional. Leave them out and the street is
  located for you; send them and they are used as given. Send both or neither.

### When an order comes back: the return code

When a delivery cannot be handed over, the courier brings the order back to the
store and the delivery moves to `returning`. To finish leaving it there, the
courier asks the store for a 4 digit code. That code is `return_verification.pincode`
on the delivery object. The courier needs it to finish the return, so your system
has to show it to the store's staff.

```json theme={null}
{
  "status": "returning",
  "return_verification": { "pincode": "4821" }
}
```

* `return_verification` is always on the object. `pincode` is a 4 digit string
  while `status` is `returning`, and `null` in every other state, so show the
  code when it is there and hide it when it is not.
* You do not need to poll for it. The `delivery.status_changed` webhook for the
  move to `returning` carries the whole delivery object, code included (see [webhooks](https://docs.listo.delivery/en/guides/webhooks.md)).
* It is the store's code, never the customer's, and it is not
  `verification.pincode`, which is the drop-off code the customer gives the
  courier. Do not send it to your customer.
* Staff should give it to the courier only once they have the order in hand.
* Each attempt has its own code. A retried delivery is a new attempt, so it
  gets a new code.
* Once the delivery is `returned` the code is spent and `pincode` goes back to
  `null`.

Listo shows the same code in your partner dashboard, on the delivery's row, while
the order is coming back and after it is back. The delivery object is still where your own system
reads it from. The field is described in
[the reference](https://docs.listo.delivery/api-reference/deliveries/fetch-a-delivery.md).

## What you get back

A `201` with the whole delivery object: the same shape you will read from
`GET /v1/deliveries/{id}`, from the list, and from every webhook. One shape
everywhere, so an object you stored at create is directly comparable to the one
an event hands you later. The full field list is in
[the reference](https://docs.listo.delivery/api-reference/deliveries/create-a-delivery.md).

Two habits worth adopting now, because v1 changes only additively: ignore fields
you do not recognise rather than failing on them, and treat every id as an
opaque string you never parse.

## When it is refused

Every refusal is the same envelope. Branch on `error.code`, never on the
message, and retry only when `error.retryable` says you may.

```json theme={null}
{
  "error": {
    "code": "out_of_service_area",
    "message": "The dropoff address is outside the delivery service area.",
    "param": null,
    "retryable": false
  }
}
```

The ones a create runs into:

| HTTP | `code` | What happened |
| - | - | - |
| 422 | `validation_failed` | A field is missing or malformed. `error.param` names it. |
| 404 | `not_found` | No store with that `store_id` exists for this key. Another account's objects look exactly like ones that never existed. |
| 400 | `quote_expired`, `quote_consumed` | The quote you bound is dead. Re-quote. |
| 422 | `out_of_service_area` | The dropoff is beyond the distance Listo covers from that store: 15 km in a straight line. It is also the answer when the courier network refuses the distance of a dropoff at the very edge of that circle. Final either way: change the address rather than retrying. |
| 422 | `address_not_found` | The street could not be located. Send coordinates. |
| 422 | `address_undeliverable` | The address was located and refused. |
| 422 | `cash_over_cap` | The cash to collect is over the limit. Take card instead. |
| 422 | `order_too_large` | The cart is more than one courier trip can carry. Split it into two deliveries. |
| 422 | `card_below_minimum` | What the customer owes is under the card minimum. |
| 422 | `card_not_ready` | No connected account can take card for that store yet. |
| 402 | `insufficient_balance` | Your balance does not cover your share of the fee. |
| 402 | `plan_required` | The store is live and cannot request couriers: it has no plan, its plan charge is pending, its plan ended, it has used all the deliveries its plan includes for the cycle, or Listo switched its couriers off. The quote refuses it too. See [plan and billing](https://docs.listo.delivery/en/guides/plan-and-billing.md). |
| 409 | `idempotency_conflict` | That key was already used with a different body, or its first request is still being processed. |
| 409 | `no_couriers_available` | Every courier near the trip is busy right now. Nothing was created. Retry the identical request, same `Idempotency-Key`, after a few minutes; it goes through once a courier is free. |
| 429 | `rate_limited` | Back off. `Retry-After` says how long. |
| 503 | `provider_unavailable` | Dispatch is temporarily unavailable. Nothing was created; retry. |

One `out_of_service_area` arrives after something was recorded. The courier
network can refuse a dropoff at the very edge of the circle on the create
itself, a moment after it priced the trip. The answer is the same `422`, with
the same body, and the identical request with the same `Idempotency-Key` gets it
again. But the delivery existed by then, so it stays: ended at `no_courier` with
`failure_reason: delivery_failed`, holding your `external_id`, listed by
`GET /v1/deliveries`, and announced by a `delivery.status_changed` webhook. The
fee debited for it was returned in full, so your ledger shows the debit and its
refund side by side, and nothing else was charged. Retrying it only meets the
same refusal. To send that order to another address, create it with a new
`external_id`.

Listo never invents a price. An order it cannot price is refused rather than
taken at a made-up number, which is why `provider_unavailable` is a distinct
answer from a validation failure: one means try again, the other means change
something.

`no_couriers_available` is not an outage either. The courier network answered,
and what it said is that nobody near the trip is free right now, which is the
same fact a delivery that ended without a courier reports as
`failure_reason: no_couriers_available`. Give it a few minutes, retry a bounded
number of times, and then tell whoever is waiting for the order. The quote
answers it the same way.

## After it is dispatched

* [`GET /v1/deliveries/{id}`](https://docs.listo.delivery/api-reference/deliveries/fetch-a-delivery) reads one
  back, and [`GET /v1/deliveries`](https://docs.listo.delivery/api-reference/deliveries/list-deliveries)
  lists them newest first, filtered by `status`, `payment_status`, `store_id`,
  `external_id`, `origin` and a created window, and paged with an opaque
  `cursor`.
* [`POST /v1/deliveries/{id}/cancellations`](https://docs.listo.delivery/api-reference/deliveries/cancel-a-delivery)
  calls it off, with a reason from Listo's own list: `customer_canceled`,
  `store_closed`, `item_unavailable`, `wrong_address`, `no_longer_needed`, or
  `other` with a `description`. It works from any non-final state, and
  cancelling an already cancelled delivery simply answers with it.
* [`POST /v1/deliveries/{id}/retries`](https://docs.listo.delivery/api-reference/deliveries/retry-a-failed-dispatch)
  re-dispatches a delivery that ended at `no_courier`. The id and your
  `external_id` survive the retry, so it stays one order to you. If the courier
  network refuses the retried delivery's distance at the edge of the service
  area, the retry answers `422 out_of_service_area`, which is final: the
  delivery stays at `no_courier` and nothing is charged.

Where a cancellation leaves the money is
[the money model](https://docs.listo.delivery/en/concepts/money.md); how to be told about all of this
without polling is [webhooks](https://docs.listo.delivery/en/guides/webhooks.md).


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