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

# Test mode

> What a test key does differently, and what it does not.

Every account has two worlds, live and test, and the key you send decides which
one you are in. There is no `?test=true`, no separate host and no header: a test
key can only ever see and change test things, and a live key can only ever see
and change live ones.

Every object says which world it belongs to:

```json theme={null}
{ "id": "dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe", "test": true }
```

## A simulated courier that actually drives

A test delivery is not a stub. It is dispatched for real and a simulated courier
takes it through the whole lifecycle on its own, at realistic timings: assigned,
on the way to the store, picked up, on the way to the customer, delivered.

Which means the things you actually need to build against are real:

* Every status transition happens, in order, over several minutes.
* Every webhook fires, including the courier position pings roughly every 20
  seconds, signed exactly the way a live one is.
* A courier turns up on the object with a name, a vehicle, a plate and a masked
  phone number.
* The tracking link works and shows the courier moving.
* Proof of delivery lands at the end: a photo, and a signature when one was
  captured.

So you can build and check your whole integration, including the parts that only
happen while an order is in motion, without ever dispatching a real person.

## Rehearse what goes wrong

The happy path is where integrations are easy. They break when an order comes
back, when nobody is free to take it, or when the courier is late. On a test key
you can choose which of those a delivery plays, by adding `simulate` to
[`POST /v1/deliveries`](https://docs.listo.delivery/api-reference/deliveries/create-a-delivery):

| `simulate` | What happens |
| - | - |
| *(absent)* | The normal run described above. |
| `returned` | The courier reaches the door, cannot hand the order over and brings it back to the store. |
| `no_courier` | The create is refused with the `409 no_couriers_available` a live create gets when every courier is busy. |
| `delayed_pickup` | The courier collects the order about 25 minutes after the create and delivers it about 5 minutes later. |

Everything else about the request is the same as any other create, and
everything that follows is the real thing: the same statuses, the same webhooks,
the same `charges`, and the same rows on `GET /v1/ledger` that a live delivery
would produce in that situation, over play money.

> [!WARNING]
>
> **Use a phone number you control**
>
> Listo messages the customer of a test delivery just as it does a live one, so
> the `dropoff.phone` of a rehearsal can receive a WhatsApp about the courier.
> Use your own number.

### `returned`: the order comes back

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "rehearsal-return-1",
  "payment_method": "prepaid",
  "order_value_cents": 19600,
  "dropoff": { "name": "Ana Torres", "phone": "+528112345678", "street": "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116" },
  "simulate": "returned"
}
```

Sent to `POST /v1/deliveries` with your usual `Idempotency-Key`. The create
answers `201` like any other. Then the delivery goes through the normal
statuses up to `en_route` and does not reach `delivered`:

```
dispatching -> picking_up -> en_route -> returning -> returned
```

* A `delivery.status_changed` webhook fires for `picking_up`, `en_route`,
  `returning` and `returned`, with `delivery.updated` pings between them. The one for
  `returning` carries the store's return code in `return_verification.pincode`,
  exactly as a live return does; see
  [the return code](https://docs.listo.delivery/en/guides/quote-and-create.md#when-an-order-comes-back-the-return-code).
* `failure_reason` is `customer_unavailable` (see
  [webhooks](https://docs.listo.delivery/en/guides/webhooks.md#statuses-worth-reacting-to) for the four
  values).
* The money follows the live return rules. On a prepaid order the part of the
  delivery fee you covered at the create comes back in full (a `subsidy_refund`
  on `GET /v1/ledger`), and the wasted trip is billed as one `provider_charge`
  of twice the trip fee, covering both legs. `charges` on the delivery lists it
  as a single `return_fee` entry (not one per leg), and `refunds` stays empty.
* A `returned` delivery is final. `POST /v1/deliveries/{id}/retries` refuses it
  with `409 not_retriable`, because the order ended; create a new delivery under
  a new `external_id` to try again.

### `no_courier`: nobody is free

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "rehearsal-nocourier-1",
  "payment_method": "prepaid",
  "order_value_cents": 19600,
  "dropoff": { "name": "Ana Torres", "phone": "+528112345678", "street": "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116" },
  "simulate": "no_courier"
}
```

The answer is `409`, the same one a live create gets:

```json theme={null}
{
  "error": {
    "code": "no_couriers_available",
    "message": "No courier is available for this trip right now. Nothing was created. Retry in a few minutes.",
    "param": null,
    "retryable": true
  }
}
```

Nothing was created: no delivery, no debit and nothing stored under your
`Idempotency-Key`. So there is no delivery to retry with
`POST /v1/deliveries/{id}/retries`; the way to retry is to send the create again.
The address is checked first, so a dropoff outside the service area gets its
`422 out_of_service_area` instead, as it would live.

Because `simulate` is part of the request, sending this `Idempotency-Key` again
without `simulate` is a fresh request and creates the delivery normally.

### `delayed_pickup`: the courier is late

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "rehearsal-late-1",
  "payment_method": "prepaid",
  "order_value_cents": 19600,
  "dropoff": { "name": "Ana Torres", "phone": "+528112345678", "street": "Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116" },
  "simulate": "delayed_pickup"
}
```

The delivery is created and delivered, but slowly. It goes through the same
statuses as the normal run: it reaches `picking_up` about a minute after the
create (that is when the courier is assigned), `en_route` about 25 minutes
after it and `delivered` about 30 minutes after it. When you send a
`pickup_ready_at`, the delivery waits in `scheduled` and the same clock runs
from that pickup time instead of from the create. Use it to see what your
system shows during a long wait and how `pickup_eta_at` and `dropoff_eta_at`
move.

### The `courier_assigned` status does not show in test mode

In test mode a delivery may go from `dispatching` straight to `picking_up`
without ever showing a `courier_assigned` status. That held in every scenario,
the normal run included. Do not wait for it: read `courier_assigned_at` or the
courier object, which are filled in.

### Rules that apply to all three

* `simulate` works on a **test key only**. A live key that sends it gets `422
  validation_failed` with `param: "simulate"`, and so does any value that is not
  one of the three. Ship the same code to production and the field is refused
  loudly rather than ignored.
* The same `Idempotency-Key` with a different `simulate` is a `409
  idempotency_conflict`, like any other change to the body, once a request has
  used the key (it created a delivery, or it is still being processed). A
  request that was refused before it created anything, such as the
  `no_courier` 409, leaves the key free: send it again with a different
  `simulate`, or none, and it is a new request.
* There is no scenario for a courier who cancels before pickup. That cannot be
  simulated, so it is not offered; test your handling of `canceled` with a
  cancellation of your own.

## Play money

The test balance is real bookkeeping over fake money. Top it up from the
[partner dashboard](https://partners.listo.delivery) with a fake top-up: no card
and no transfer, the balance simply goes up.

Everything downstream of that behaves like the live ledger:

* Your share of each delivery fee is debited at create, and a create the balance
  cannot cover is refused with `402 insufficient_balance` exactly as it would be
  live.
* A cancelled or failed delivery refunds it, and bills the same wasted-trip
  charge for a late cancellation.
* Cash orders credit their order value on delivery and hold it, then release it
  to `available_cents` overnight once it is a day old, rather than on the weekly
  settlement cycle, so you can watch both states without waiting a week.
* Withdrawals can be requested and resolve themselves to `paid` about three
  minutes later. No money is sent anywhere, and the `withdrawal.updated` webhook
  fires for real.

`GET /v1/ledger` reads back on a test key too, with every row carrying
`"test": true`, so you can build and check your reconciliation against play
money before a peso is at stake.

Card orders in test mode take test cards only.

Test stores carry no plan. `plan` is `null` on every one of them, nothing is
ever charged for one, and no quote or delivery is refused with
`plan_required`. Plan Listo starts with your first live store; see
[plan and billing](https://docs.listo.delivery/en/guides/plan-and-billing.md).

## The two worlds cannot see each other

A test key asking for a live object gets `404 not_found`, and the other way
round. It is not a permission error, because from a key's point of view the
other world's objects never existed.

That extends to the things you name yourself: `external_id` and
`Idempotency-Key` are unique per mode. Your staging system and your production
system can both dispatch `order-841` on the same day without colliding, so you
never need mode-specific prefixes.

> [!WARNING]
>
> **Set the test endpoint separately**
>
> Webhook endpoints and signing secrets are per mode too. Setting the live pair
> does not give you test events, and pointing both modes at one endpoint means you
> cannot tell a rehearsal from a real order except by reading `test` on the
> payload. Use two endpoints.

## What test mode is not for

* **It is not a load environment.** The same rate limits apply, and the
  simulated courier takes the same few minutes a real one does.
* **It is not a sandbox for addresses.** The service area is enforced the same
  way, so a test dropoff still has to be within reach of the test store you
  dispatch from.
* **It does not expire.** Leave your staging environment pointed at a test key
  permanently.

Everything you should check before switching a key is in
[go live](https://docs.listo.delivery/en/guides/go-live.md).


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