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

# Modo de prueba

> Qué hace distinto una llave de prueba, y qué no.

Cada cuenta tiene dos mundos, real y de prueba, y la llave que mandas decide en
cuál estás. No hay `?test=true`, no hay otro host y no hay encabezado: una llave
de prueba solo puede ver y cambiar cosas de prueba, y una llave real solo puede
ver y cambiar cosas reales.

Cada objeto dice a qué mundo pertenece:

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

## Un repartidor simulado que de verdad maneja

Un envío de prueba no es un maniquí. Se despacha de verdad y un repartidor
simulado lo lleva por todo el ciclo de vida por su cuenta, con tiempos
realistas: asignado, en camino a la tienda, recogido, en camino al cliente,
entregado.

Lo que significa que las cosas contra las que de verdad necesitas programar son
reales:

* Cada cambio de estado ocurre, en orden, a lo largo de varios minutos.
* Cada webhook se dispara, incluidos los avisos de posición del repartidor
  aproximadamente cada 20 segundos, firmados igual que uno real.
* Aparece un repartidor en el objeto, con nombre, vehículo, placas y un teléfono
  enmascarado.
* El enlace de rastreo funciona y muestra al repartidor moviéndose.
* La prueba de entrega llega al final: una foto, y una firma cuando se capturó
  alguna.

Así que puedes construir y revisar toda tu integración, incluidas las partes que
solo ocurren mientras un pedido va en camino, sin despachar nunca a una persona
real.

## Ensaya lo que sale mal

El camino feliz es donde las integraciones son fáciles. Se rompen cuando un
pedido regresa, cuando nadie está libre para tomarlo o cuando el repartidor se
tarda. Con una llave de prueba puedes elegir cuál de esas situaciones juega un
envío, agregando `simulate` a
[`POST /v1/deliveries`](https://docs.listo.delivery/es/api-reference/deliveries/create-a-delivery):

| `simulate` | Qué pasa |
| - | - |
| *(ausente)* | La corrida normal descrita arriba. |
| `returned` | El repartidor llega a la puerta, no puede entregar el pedido y lo regresa a la tienda. |
| `no_courier` | La creación se rechaza con el `409 no_couriers_available` que recibe una creación real cuando todos los repartidores están ocupados. |
| `delayed_pickup` | El repartidor recoge el pedido unos 25 minutos después de crearlo y lo entrega unos 5 minutos más tarde. |

Todo lo demás de la solicitud es igual que en cualquier otra creación, y todo lo
que sigue es de verdad: los mismos estados, los mismos webhooks, los mismos
`charges` y las mismas filas en `GET /v1/ledger` que produciría un envío real en
esa situación, con dinero ficticio.

> [!WARNING]
>
> **Usa un teléfono que controles**
>
> Listo le escribe al cliente de un envío de prueba igual que al de uno real, así
> que el `dropoff.phone` de un ensayo puede recibir un WhatsApp sobre el
> repartidor. Usa tu propio número.

### `returned`: el pedido regresa

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "ensayo-devolucion-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"
}
```

Se manda a `POST /v1/deliveries` con tu `Idempotency-Key` de siempre. La
creación responde `201` como cualquier otra. Después el envío pasa por los
estados normales hasta `en_route` y no llega a `delivered`:

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

* Se dispara un webhook `delivery.status_changed` para `picking_up`, `en_route`,
  `returning` y `returned`, con avisos `delivery.updated` entre ellos. El de
  `returning` trae el código de devolución de la tienda en
  `return_verification.pincode`, igual que una devolución real; ve
  [el código de devolución](https://docs.listo.delivery/es/guides/quote-and-create.md#cuando-un-pedido-regresa-el-código-de-devolución).
* `failure_reason` es `customer_unavailable` (los cuatro valores están en
  [webhooks](https://docs.listo.delivery/es/guides/webhooks.md#estados-a-los-que-vale-la-pena-reaccionar)).
* El dinero sigue las reglas de una devolución real. En un pedido prepagado, la
  parte de la tarifa que cubriste al crearlo regresa completa (un
  `subsidy_refund` en `GET /v1/ledger`), y el viaje desperdiciado se cobra como
  un solo `provider_charge` del doble de la tarifa del viaje, que cubre los dos
  tramos. `charges` en el envío lo lista como una sola entrada `return_fee` (no
  una por tramo), y `refunds` queda vacío.
* Un envío `returned` es final. `POST /v1/deliveries/{id}/retries` lo rechaza
  con `409 not_retriable`, porque el pedido terminó; crea un envío nuevo con
  otro `external_id` para volver a intentar.

### `no_courier`: nadie está libre

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "ensayo-sin-repartidor-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"
}
```

La respuesta es `409`, la misma que recibe una creación real:

```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
  }
}
```

No se creó nada: ni envío, ni cargo, ni nada guardado bajo tu `Idempotency-Key`.
Así que no hay un envío que reintentar con `POST /v1/deliveries/{id}/retries`;
la forma de reintentar es mandar la creación otra vez. La dirección se revisa
primero, así que un destino fuera del área de cobertura recibe su `422
out_of_service_area`, como en real.

Como `simulate` es parte de la solicitud, mandar esta `Idempotency-Key` otra vez
sin `simulate` es una solicitud nueva y crea el envío con normalidad.

### `delayed_pickup`: el repartidor se tarda

```json theme={null}
{
  "store_id": "st_...",
  "external_id": "ensayo-tarde-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"
}
```

El envío se crea y se entrega, pero despacio. Pasa por los mismos estados que
la corrida normal: llega a `picking_up` como un minuto después de la creación
(ahí se asigna el repartidor), a `en_route` unos 25 minutos después de ella y a
`delivered` unos 30 minutos después. Cuando mandas un `pickup_ready_at`, el
envío espera en `scheduled` y el mismo reloj corre desde esa hora de recolección
en lugar de desde la creación. Úsalo para ver qué muestra tu sistema durante una
espera larga y cómo se mueven `pickup_eta_at` y `dropoff_eta_at`.

### El estado `courier_assigned` no aparece en modo de prueba

En modo de prueba un envío puede pasar de `dispatching` directo a `picking_up`
sin mostrar nunca un estado `courier_assigned`. Pasó en todos los escenarios,
la corrida normal incluida. No lo esperes: lee `courier_assigned_at` o el objeto
del repartidor, que sí se llenan.

### Reglas que aplican a los tres

* `simulate` funciona **solo con una llave de prueba**. Una llave real que lo
  manda recibe `422 validation_failed` con `param: "simulate"`, igual que
  cualquier valor que no sea uno de los tres. Si llevas el mismo código a
  producción, el campo se rechaza en voz alta en lugar de ignorarse.
* La misma `Idempotency-Key` con un `simulate` distinto es un `409
  idempotency_conflict`, como cualquier otro cambio al cuerpo, una vez que una
  solicitud ya usó la llave (creó una entrega, o todavía se está procesando).
  Una solicitud rechazada antes de crear algo, como el `409` de `no_courier`,
  deja la llave libre: mándala otra vez con otro `simulate`, o sin él, y es una
  solicitud nueva.
* No hay un escenario para un repartidor que cancela antes de recoger. Eso no se
  puede simular, así que no se ofrece; prueba cómo manejas `canceled` con una
  cancelación tuya.

## Dinero ficticio

El saldo de prueba es contabilidad real sobre dinero falso. Recárgalo desde el
[panel de socios](https://partners.listo.delivery) con una recarga ficticia: sin
tarjeta y sin transferencia, el saldo simplemente sube.

Todo lo que viene después se comporta como el historial real:

* Tu parte de cada tarifa de envío se carga al crear, y una creación que el
  saldo no alcanza a cubrir se rechaza con `402 insufficient_balance` igual que
  lo haría en real.
* Un envío cancelado o fallido la reembolsa, y cobra el mismo cargo por viaje
  desperdiciado en una cancelación tardía.
* Los pedidos en efectivo abonan su valor del pedido al entregarse y lo
  retienen, y luego lo liberan a `available_cents` de un día para otro una vez
  que cumple un día, en lugar de esperar al ciclo semanal de liquidación, así
  que puedes ver los dos estados sin esperar una semana.
* Los retiros se pueden solicitar y se resuelven solos a `paid` unos tres
  minutos después. No se manda dinero a ningún lado, y el webhook
  `withdrawal.updated` se dispara de verdad.

`GET /v1/ledger` también responde con una llave de prueba, y cada fila trae
`"test": true`, así que puedes armar y revisar tu conciliación con dinero
ficticio antes de que haya un peso de por medio.

Los pedidos con tarjeta en modo de prueba solo aceptan tarjetas de prueba.

Las tiendas de prueba no llevan plan. `plan` viene en `null` en todas, nunca se
cobra nada por uno, y ninguna cotización ni envío se rechaza con
`plan_required`. Plan Listo empieza con tu primera tienda en modo real; ve
[plan y cobro](https://docs.listo.delivery/es/guides/plan-and-billing.md).

## Los dos mundos no se ven entre sí

Una llave de prueba que pide un objeto real recibe `404 not_found`, y al revés
igual. No es un error de permisos: desde el punto de vista de una llave, los
objetos del otro mundo nunca existieron.

Eso alcanza también a lo que tú nombras: `external_id` e `Idempotency-Key` son
únicos por modo. Tu sistema de staging y tu sistema de producción pueden
despachar `order-841` el mismo día sin chocar, así que nunca necesitas prefijos
por modo.

> [!WARNING]
>
> **Configura el endpoint de prueba por separado**
>
> Los endpoints de webhooks y los secretos de firma también son por modo.
> Configurar el par real no te da eventos de prueba, y apuntar los dos modos al
> mismo endpoint significa que no puedes distinguir un ensayo de un pedido real
> más que leyendo `test` en el payload. Usa dos endpoints.

## Para qué no sirve el modo de prueba

* **No es un ambiente de carga.** Aplican los mismos límites de tasa, y el
  repartidor simulado se tarda los mismos minutos que uno real.
* **No es un sandbox de direcciones.** El área de cobertura se aplica igual, así
  que un destino de prueba tiene que seguir estando al alcance de la tienda de
  prueba desde la que despachas.
* **No caduca.** Deja tu ambiente de staging apuntado a una llave de prueba de
  forma permanente.

Todo lo que conviene revisar antes de cambiar de llave está en
[sal a producción](https://docs.listo.delivery/es/guides/go-live.md).


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