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

# Cotiza y crea un envío

> Calcula el precio de un envío y después convierte la cotización en un envío real.

Despachar un pedido son dos llamadas. `POST /v1/quotes` pregunta cuánto cuesta un
envío a una dirección dada y aparta ese precio para ti; `POST /v1/deliveries`
regresa la cotización y le cobra al pedido el precio que te mostramos.

La cotización es opcional. Tómala cuando una persona vaya a ver el número y
decidir algo, y omítela cuando no haya nadie: una creación sin `quote_id` se
cotiza sola, en línea, al precio del momento y en una sola llamada.

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

## La cotización

Una cotización fija el precio en el momento en que la pides y lo guarda para que
lo uses. No es un apartado de repartidor. No se despacha nada, no se mueve
dinero, y dejar que una expire no te cuesta nada.

Si usas una cotización, al pedido se le cobra el `fee_cents` y el reparto que te
mostramos. La red de repartidores reserva el viaje cuando se crea el envío, y el
precio que reserva puede diferir un poco del cotizado. Reservamos la misma
cotización que viste siempre que siga vigente, así que esto es poco común. Cuando
pasa, la diferencia aparece en tu historial como su propia línea
`requote_adjustment` (negativa si el viaje reservado costó más, positiva si costó
menos). Nunca cambia el `fee_cents`, el reparto ni el total que ve tu cliente.

Envías la tienda, el punto de entrega y lo suficiente del pedido para poder
cotizarlo: el método de pago y el valor del pedido cambian los dos lo que cuesta
un envío, así que una cotización tomada sin ellos es la cotización de otro
pedido. Responde con:

| Campo | Qué es |
| - | - |
| `id` | El `qt_...` que regresas al crear. |
| `fee_cents` | La tarifa de envío completa. |
| `business_fee_cents`, `customer_fee_cents` | Cómo se reparte esa tarifa entre tú y el cliente. Las dos siempre suman `fee_cents`. |
| `cash_to_collect_cents` | En una cotización en efectivo, lo que el repartidor cobrará en la puerta. `null` en los demás casos. |
| `vehicle` | `moto` o `car`: para qué viaje es `fee_cents`. Consulta [enviarlo en coche](#enviarlo-en-coche). |
| `pickup_duration_min` | Aproximadamente cuánto falta para que un repartidor llegue a la tienda. |
| `dropoff_eta_at` | Cuándo llegaría el pedido, como marca de tiempo absoluta. |
| `expires_at` | Cuándo se deja de respetar el precio. |

`expires_at` está a lo mucho a quince minutos y con frecuencia a menos. De ahí se
siguen dos reglas, y las dos son rechazos, no sorpresas:

* Crear contra una cotización expirada devuelve `400 quote_expired`. Pide una
  nueva e intenta de nuevo; no se creó nada.
* Una cotización es de un solo uso. Gastarla dos veces devuelve
  `400 quote_consumed`, que es como se ve desde aquí un doble clic en el
  formulario.

> [!WARNING]
>
> **La creación tiene que coincidir con la cotización**
>
> Vincular una cotización significa que el precio ya quedó, así que la creación no
> puede contradecir aquello con lo que se calculó ese precio. `payment_method`,
> `order_value_cents`, `business_fee_share`, `business_fee_cap_cents`, la `street`
> de entrega y el `apt_unit` de entrega tienen que coincidir con la cotización, o
> la creación se rechaza con `validation_failed` nombrando el campo que no
> concuerda. Si cambias cualquiera de ellos, necesitas una cotización nueva.
>
> El vehículo también tiene que coincidir, y se compara como viaje completo, no
> solo el campo `vehicle`: una creación cuyo manifiesto trae un artículo de
> tamaño `xlarge` es un viaje en coche diga lo que diga `vehicle`, así que
> vincularla a una cotización calculada en moto se rechaza, nombrando
> `manifest.items`. Si dejas `vehicle` fuera, la creación hereda el de la
> cotización.
>
> El mensaje de ese rechazo dice por qué y qué hacer: cuando lo que no concuerda es
> el reparto, te dice que envíes el mismo reparto o pidas una cotización nueva.
>
> Dejar un campo fuera no es contradecir nada: lo que omites se hereda de la
> cotización. Mandar `business_fee_cap_cents` al crear contra una cotización que se
> tomó *sin* tope no cuenta como agregarlo, sino como contradecirla, así que
> ponle el tope también a la cotización.
>
> `tip_cents` es la excepción, y la única: las cotizaciones no tienen campo de
> propina, una propina nunca cambia una tarifa, y cualquier propina que mandes al
> crear es compatible con cualquier cotización que vincules.

## La creación

Obligatorio en cada creación: `store_id`, `external_id`, `payment_method`,
`order_value_cents`, y un `dropoff` con al menos `name`, `phone` y `street`. Todo
lo demás tiene un valor por defecto.

`order_value_cents` es el valor de la comida o de la mercancía, y te toca a ti
declararlo. Nunca se suma a partir del manifiesto, y nunca incluye la tarifa de
envío.

### `Idempotency-Key` es obligatorio

Envía un encabezado `Idempotency-Key`, de máximo 255 caracteres, en cada
creación. No es opcional, y una creación sin él se rechaza.

Derívalo de tu pedido, no del intento. Eso es lo que hace que valga la pena el
encabezado: cuando una creación se va a timeout no sabes si se despachó un
repartidor, y repetirla con la misma llave responde con el envío original en vez
de mandar un segundo. La respuesta original se repite durante 24 horas, código de
estado incluido.

Reutilizar una llave con un cuerpo *distinto* devuelve
`409 idempotency_conflict`. Es a propósito: significa que se mandaron dos
pedidos distintos bajo una misma llave, y responder con cualquiera de los dos
estaría mal.

Una repetición que llega mientras la primera solicitud con esa llave todavía se
está procesando también devuelve `409 idempotency_conflict`. No creó nada, y no
significa que la primera haya fallado: esa solicitud sigue hasta su propia
respuesta, y en cuanto responde, la misma solicitud con la misma llave la repite.

### `external_id` es cómo lo vas a reconocer

`external_id` es tu propia referencia del pedido. Regresa en cada respuesta y en
cada webhook, se puede consultar con `GET /v1/deliveries?external_id=...`, y es
único por modo de llave, así que tu sistema de staging y el de producción pueden
usar los dos `order-841` sin chocar nunca. Mandar uno que ya usaste en ese modo
se rechaza.

No tienes que guardar el id `dlv_...` de Listo si prefieres no hacerlo.

### Métodos de pago

| `payment_method` | Quién cobra el dinero | Dónde empieza el envío |
| - | - | - |
| `prepaid` | Tú, ya lo cobraste en tu propio checkout. Listo no le cobra a tu cliente. | `dispatching`, ya `paid` |
| `cash` | El repartidor, en la puerta. Ve [efectivo en la puerta](https://docs.listo.delivery/es/concepts/cash-at-the-door.md). | `dispatching`, y `payment.status` se queda en `unpaid` |
| `card` | Listo, mediante un enlace de pago que Listo mismo le manda a tu cliente. | `awaiting_payment` hasta que pague, y luego se despacha solo |

En `card` la respuesta trae `payment.url`: el enlace con la marca que Listo le
manda a tu cliente por su cuenta, en cuanto se crea el pedido (consulta
[lo que Listo le manda a tu cliente](#lo-que-listo-le-manda-a-tu-cliente)). Puedes
mostrarlo en tu pantalla o guardarlo, pero no tienes que mandarlo, y no debes:
tu cliente recibiría el mismo enlace dos veces, de dos remitentes. El envío se
queda ahí hasta que se pague, y después se despacha solo. El enlace desaparece
del objeto en cuanto se paga o se cancela, porque un enlace muerto en un payload
es peor que ninguno, y Listo no manda nada de un enlace que ya no está vigente.

Con `prepaid` la tarifa completa es tuya: no hay a quién cobrarle una parte de
cliente en un pedido cuyo cliente Listo nunca cobra. Por eso la parte es siempre
100 y `business_fee_cap_cents` se ignora, y cómo se nota depende del endpoint:

* `POST /v1/quotes` con `prepaid` rechaza un `business_fee_share` distinto de 100
  con `422 validation_failed` nombrando `business_fee_share`. Envía 100 o déjalo
  fuera: sin campos de reparto, la cotización se calcula con 100, sin importar el
  reparto por defecto de tu tienda. Una cotización que no indica `payment_method`
  se calcula exactamente igual, porque la única entrega que puede enlazarla es una
  `prepaid`: también rechaza una parte distinta de 100, con un mensaje que lo dice
  y que explica que puedes indicar el `payment_method`. Para repartir la tarifa con
  tu cliente, cotiza como `cash` o `card`.
* `POST /v1/deliveries` con `prepaid` y sin `quote_id` fuerza la parte a 100 y
  crea el pedido.
* `POST /v1/deliveries` con `prepaid` y un `quote_id` necesita una cotización que
  se haya pedido con `payment_method: prepaid` o sin `payment_method`, y ambas se
  calculan siempre con 100. Cualquier otra cotización se rechaza con
  `422 validation_failed`, nombrando lo primero que no coincide. Una cotización
  pedida como `cash` o `card` se rechaza nombrando `payment_method`. Una
  cotización cuyo reparto no es 100 sin tope (una guardada antes de esta regla) se
  rechaza nombrando `business_fee_share` (o `business_fee_cap_cents`), con un
  mensaje que dice que pidas una cotización nueva como `prepaid`.

Ve [el modelo de dinero](https://docs.listo.delivery/es/concepts/money.md).

### Lo que Listo le manda a tu cliente

Listo le escribe por su cuenta al cliente de tus pedidos, desde su propio número
de WhatsApp y en español, así que no tienes que construir nada para mantenerlo
informado. Salen dos cosas:

* **El enlace de pago, en los pedidos `card`.** En cuanto se crea un pedido
  `card` y queda esperando el pago, Listo le manda al cliente un WhatsApp con su
  nombre, el nombre de tu tienda, lo que debe y el enlace `payment.url`, y le
  dice que el repartidor sale en cuanto pague. Si ese WhatsApp no le llega (el
  número no tiene WhatsApp, o el mensaje falla), Listo le manda el mismo enlace
  por SMS, desde un código corto, para que lo reciba de todos modos. Se manda una
  vez por pedido, sin que tengas que llamar nada. Un enlace que ya no está
  vigente, porque el pedido se pagó o se canceló, no se manda.
* **Los mensajes del repartidor, en todos los pedidos.** Cuando se asigna un
  repartidor, cuando va en camino, cuando está llegando y cuando se entrega el
  pedido, Listo le manda al cliente un WhatsApp desde el mismo número, con el
  enlace de seguimiento y, si lo pediste, el código de entrega. Esto incluye los
  pedidos `prepaid` y `cash`: haber cobrado en tu propio checkout no impide que
  Listo le diga al cliente dónde va su pedido.

No mandes nada tuyo para ninguna de las dos cosas, o el cliente lo recibe dos
veces. Todo lo demás que le digas, tu confirmación del pedido o tu recibo, sigue
siendo tuyo. Nada de tu saldo, del reparto de la tarifa ni de lo que te cuesta un
envío aparece en los mensajes de Listo.

### Propina para el repartidor

`tip_cents` agrega una propina para el repartidor, en centavos, encima de todo lo
demás del pedido. Le llega completa al repartidor: Listo no le suma nada ni se
queda con nada.

Es opcional, su valor por defecto es `0`, y tiene un tope de `100000`
(MX\$1,000). Cualquier cosa arriba de eso, o por debajo de cero, se rechaza con
`validation_failed` nombrando `tip_cents` antes de cotizar el pedido, así que una
propina imposible no te cuesta nada.

Una propina no es parte de `fee_cents` y no se reparte con tu cliente. Tu parte
de la tarifa y la propina salen juntas de tu saldo, como un solo movimiento, al
crear el envío; y si el envío nunca ocurre, también regresan juntas: un pedido
`canceled`, `returned` o `no_courier` te reembolsa la propina junto con tu parte
de la tarifa.

> [!WARNING]
>
> **Las propinas son solo para pedidos prepagados**
>
> `tip_cents` se acepta con `payment_method: prepaid`. Una propina mayor a cero en
> un pedido `cash` o `card` se rechaza con `validation_failed` nombrando
> `tip_cents`.
>
> Esos dos pedidos son en los que Listo cobra el dinero, y ahí una propina
> declarada tendría la forma equivocada en ambos casos. En un pedido en efectivo al
> repartidor se le da la propina en mano en la puerta, por la misma persona que ya
> le está entregando dinero, y una propina declarada cambiaría lo que el repartidor
> tiene que cobrar y contaría para el límite de efectivo. En un pedido con tarjeta
> tu cliente paga con un enlace de Listo, así que una propina que tú fondeas encima
> de un cargo que él pagó es una segunda propina, distinta. El prepago es el único
> método en el que ya arreglaste cuentas con tu cliente por tu lado, así que la
> propina es tuya sin ambigüedad.

Una propina se decide una sola vez, al crear, y no se puede subir después. No hay
ninguna llamada que le agregue propina a un envío que ya va en camino.

### Programación

Si dejas fuera `pickup_ready_at`, el repartidor se llama de inmediato. Si lo
mandas, el pedido queda programado: el repartidor se agenda ahora, para una
recolección después.

Tiene que estar a por lo menos 10 minutos y a lo mucho a 29 días. Diez minutos es
poco a propósito, porque "la comida sale en quince" es el caso normal para una
máquina, no el excepcional.

Un pedido programado se puede mover con `PATCH /v1/deliveries/{id}` mientras
siga en `scheduled`, incluso a ahora mismo, lo cual lo despacha. Una vez que
salió de `scheduled` la ventana queda fija y la llamada devuelve
`409 not_reschedulable`.

### Enviarlo en coche

`vehicle` es `moto`, el valor por defecto, o `car`. Manda `car` para un pedido
que no cabe en una moto.

* Un coche es un sedán. Podemos garantizar un coche, nunca una camioneta.
* Hay menos coches que motos, así que un coche puede tardar más en asignarse.
* Un coche le suma un recargo a la tarifa de envío. Va dentro de `fee_cents` y
  se reparte como el resto de la tarifa, así que el precio que te cotizamos es
  el precio completo, sin una línea aparte.

Cada artículo de `manifest.items` también puede llevar un `size`: `small` (el
valor por defecto), `medium`, `large` o `xlarge`. Un artículo de tamaño `xlarge`
hace que el viaje sea en coche aunque `vehicle` diga `moto`, porque no cabe en
una moto. Ningún otro tamaño lo hace, y tampoco la cantidad de artículos: dos
artículos `large` siguen siendo un viaje en moto. Manda en la cotización el
mismo `vehicle` y el mismo manifiesto con los que vas a crear, para que el
precio que muestras sea el del viaje que reservas.

La cotización y el envío responden con `vehicle`: en qué se cotizó y se despachó
el viaje, sea lo que sea que lo hizo en coche. No es el vehículo que llega. Ese
es `courier.vehicle_type` en el envío, que de vez en cuando puede ser un coche
en un viaje `moto` sin que cambie la tarifa.

Un carrito demasiado grande para un solo viaje de repartidor se rechaza con
`422 order_too_large`, nombrando `manifest.items`, antes de cotizar o despachar
nada. Divídelo en dos envíos.

### El detalle opcional

* `manifest.items` es una lista de `{name, quantity, size}` para que el
  repartidor coteje. Es descriptiva: el dinero sale de `order_value_cents`, y
  lo único que un artículo puede cambiar es el vehículo, con su `size` (consulta
  [enviarlo en coche](#enviarlo-en-coche)).
* `handoff` es `meet_at_door` (el valor por defecto), `leave_at_door` o
  `meet_outside`. Los pedidos en efectivo siempre se entregan en persona.
* `dropoff.notes` son instrucciones por pedido para esa dirección, de máximo 280
  caracteres. Las instrucciones permanentes de la *tienda* van en la tienda, como
  `pickup_notes`.
* `verification.dropoff_pincode: true` pide un código que el cliente le tiene que
  dar al repartidor. Regresa en el envío como `verification.pincode` en cuanto un
  repartidor tiene uno. Listo también se lo da a tu cliente en sus propios
  mensajes del repartidor, así que solo necesitas mostrarlo tú si lo quieres en
  tu pantalla.
* `dropoff.lat` y `dropoff.lng` son opcionales. Si los dejas fuera, la calle se
  ubica por ti; si los mandas, se usan tal cual. Manda los dos o ninguno.

### Cuando un pedido regresa: el código de devolución

Cuando un envío no se puede entregar, el repartidor regresa el pedido a la
tienda y el envío pasa a `returning`. Para terminar de dejarlo ahí, el
repartidor le pide a la tienda un código de 4 dígitos. Ese código de devolución es
`return_verification.pincode` en el objeto del envío. El repartidor lo necesita
para terminar la devolución, así que tu sistema tiene que mostrárselo al personal
de la tienda.

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

* `return_verification` siempre viene en el objeto. `pincode` es una cadena de 4
  dígitos mientras `status` es `returning`, y `null` en cualquier otro estado, así
  que muestra el código cuando venga y escóndelo cuando no.
* No necesitas sondear. El webhook `delivery.status_changed` del paso a
  `returning` trae el objeto completo del envío, código incluido (ve [webhooks](https://docs.listo.delivery/es/guides/webhooks.md)).
* Es el código de la tienda, nunca el del cliente, y no es `verification.pincode`,
  que es el código de entrega que el cliente le da al repartidor. No se lo
  mandes a tu cliente.
* El personal debe dárselo al repartidor solo cuando ya tenga el pedido en la
  mano.
* Cada intento tiene su propio código. Un envío reintentado es un intento nuevo,
  así que recibe un código nuevo.
* Cuando el envío queda en `returned`, el código ya se usó y `pincode` vuelve a
  `null`.

Listo muestra el mismo código en tu panel de partner, en la fila del envío,
mientras el pedido regresa y después de que regresó. El objeto del envío sigue siendo de donde lee el
código tu propio sistema. El campo está descrito en
[la referencia](https://docs.listo.delivery/es/api-reference/deliveries/fetch-a-delivery.md).

## Qué recibes de vuelta

Un `201` con el objeto de envío completo: la misma forma que vas a leer de
`GET /v1/deliveries/{id}`, del listado y de cada webhook. Una sola forma en todos
lados, así que un objeto que guardaste al crear es directamente comparable con el
que un evento te entrega después. La lista completa de campos está en
[la referencia](https://docs.listo.delivery/es/api-reference/deliveries/create-a-delivery.md).

Dos costumbres que vale la pena adoptar desde ahora, porque v1 solo cambia de
forma aditiva: ignora los campos que no reconozcas en lugar de fallar con ellos,
y trata cada id como una cadena opaca que nunca analizas.

## Cuándo se rechaza

Cada rechazo usa el mismo sobre. Ramifica según `error.code`, nunca según el
mensaje, y reintenta solo cuando `error.retryable` diga que puedes.

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

Los que se encuentra una creación:

| HTTP | `code` | Qué pasó |
| - | - | - |
| 422 | `validation_failed` | Falta un campo o está mal formado. `error.param` lo nombra. |
| 404 | `not_found` | No existe ninguna tienda con ese `store_id` para esta llave. Los objetos de otra cuenta se ven exactamente igual que los que nunca existieron. |
| 400 | `quote_expired`, `quote_consumed` | La cotización que vinculaste ya no sirve. Vuelve a cotizar. |
| 422 | `out_of_service_area` | La entrega queda más allá de la distancia que Listo cubre desde esa tienda: 15 km en línea recta. También es la respuesta cuando la red de repartidores rechaza la distancia de una entrega justo en la orilla de ese círculo. En los dos casos es definitivo: cambia la dirección en lugar de reintentar. |
| 422 | `address_not_found` | No se pudo ubicar la calle. Manda coordenadas. |
| 422 | `address_undeliverable` | La dirección se ubicó y se rechazó. |
| 422 | `cash_over_cap` | El efectivo por cobrar rebasa el límite. Cobra con tarjeta. |
| 422 | `order_too_large` | El carrito es más de lo que cabe en un solo viaje de repartidor. Divídelo en dos envíos. |
| 422 | `card_below_minimum` | Lo que debe el cliente está por debajo del mínimo para tarjeta. |
| 422 | `card_not_ready` | Todavía no hay una cuenta conectada que pueda cobrar con tarjeta para esa tienda. |
| 402 | `insufficient_balance` | Tu saldo no cubre tu parte de la tarifa. |
| 402 | `plan_required` | La tienda está en modo real y no puede pedir repartidores: no tiene plan, el cargo de su plan está pendiente, su plan terminó, ya usó todos los envíos que incluye su plan en el ciclo, o Listo le apagó los repartidores. La cotización también se rechaza. Ve [plan y cobro](https://docs.listo.delivery/es/guides/plan-and-billing.md). |
| 409 | `idempotency_conflict` | Esa llave ya se usó con un cuerpo distinto, o su primera solicitud todavía se está procesando. |
| 409 | `no_couriers_available` | Todos los repartidores cerca del viaje están ocupados en este momento. No se creó nada. Repite la misma solicitud, con la misma `Idempotency-Key`, en unos minutos; pasa en cuanto haya un repartidor libre. |
| 429 | `rate_limited` | Bájale al ritmo. `Retry-After` dice cuánto. |
| 503 | `provider_unavailable` | El despacho no está disponible por el momento. No se creó nada; reintenta. |

Un `out_of_service_area` llega después de que algo quedó registrado. La red de
repartidores puede rechazar una entrega justo en la orilla del círculo en la
creación misma, un momento después de cotizar el viaje. La respuesta es el mismo
`422`, con el mismo cuerpo, y la misma solicitud con la misma `Idempotency-Key`
la recibe otra vez. Pero para entonces el envío ya existía, así que se queda:
terminado en `no_courier` con `failure_reason: delivery_failed`, con tu
`external_id`, listado por `GET /v1/deliveries` y anunciado por un webhook
`delivery.status_changed`. La tarifa que se descontó por él se devolvió completa,
así que tu libro muestra el cargo y su devolución uno junto al otro, y no se
cobró nada más. Reintentarlo solo vuelve a encontrar el mismo rechazo. Para
mandar ese pedido a otra dirección, créalo con un `external_id` nuevo.

Listo nunca inventa un precio. Un pedido que no puede cotizar se rechaza en lugar
de tomarse a un número inventado, y por eso `provider_unavailable` es una
respuesta distinta de una falla de validación: una significa intenta otra vez, la
otra significa cambia algo.

`no_couriers_available` tampoco es una caída. La red de repartidores respondió, y
lo que dijo es que nadie cerca del viaje está libre en este momento, que es el
mismo hecho que un envío terminado sin repartidor reporta como
`failure_reason: no_couriers_available`. Dale unos minutos, reintenta un número
limitado de veces y después avísale a quien espera el pedido. La cotización lo
responde igual.

## Después de despacharlo

* [`GET /v1/deliveries/{id}`](https://docs.listo.delivery/es/api-reference/deliveries/fetch-a-delivery) lee uno, y
  [`GET /v1/deliveries`](https://docs.listo.delivery/es/api-reference/deliveries/list-deliveries) los lista del
  más reciente al más antiguo, filtrados por `status`, `payment_status`,
  `store_id`, `external_id`, `origin` y una ventana de creación, y paginados con
  un `cursor` opaco.
* [`POST /v1/deliveries/{id}/cancellations`](https://docs.listo.delivery/es/api-reference/deliveries/cancel-a-delivery)
  lo cancela, con un motivo de la lista de Listo: `customer_canceled`,
  `store_closed`, `item_unavailable`, `wrong_address`, `no_longer_needed`, u
  `other` con una `description`. Funciona desde cualquier estado no final, y
  cancelar un envío ya cancelado simplemente responde con él.
* [`POST /v1/deliveries/{id}/retries`](https://docs.listo.delivery/es/api-reference/deliveries/retry-a-failed-dispatch)
  vuelve a despachar un envío que terminó en `no_courier`. El id y tu
  `external_id` sobreviven al reintento, así que para ti sigue siendo un solo
  pedido. Si la red de repartidores rechaza la distancia del envío reintentado
  en la orilla de la zona de servicio, el reintento responde
  `422 out_of_service_area`, que es definitivo: el envío se queda en
  `no_courier` y no se cobra nada.

Dónde queda el dinero cuando hay una cancelación es
[el modelo de dinero](https://docs.listo.delivery/es/concepts/money.md); cómo enterarte de todo esto sin
sondear es [webhooks](https://docs.listo.delivery/es/guides/webhooks.md).


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