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

# Fondea tu saldo y retira

> De dónde sale el dinero de un envío, y cómo sacarlo.

Tu cuenta tiene un saldo por modo. De ahí se pagan los envíos, ahí entra el
efectivo que cobran tus repartidores, y los retiros mueven lo que queda a tu
banco. Esta página es la mitad operativa; lo que *significa* cada movimiento
está en [el modelo de dinero](https://docs.listo.delivery/es/concepts/money.md).

## Dos números

```json theme={null}
{ "balance_cents": 412050, "available_cents": 268400, "currency": "MXN" }
```

`balance_cents` es todo lo que tiene la cuenta. `available_cents` es la parte
que puede salir hoy.

Hay dos motivos por los que se retiene dinero fuera de `available_cents`, y en
los dos casos ese dinero se puede gastar en envíos mientras espera:

* **El efectivo que cobraron tus repartidores.** Cuando se entrega un pedido en
  efectivo, el valor del pedido se abona a tu saldo de inmediato. Se vuelve
  retirable una vez que ese efectivo ya se le liquidó a Listo, en un ciclo
  semanal.
* **El dinero que metiste con tarjeta.** Una recarga con tarjeta se retiene
  **180 días** desde el cargo, que es la ventana en la que un pago con tarjeta
  todavía se puede disputar. Una transferencia SPEI no tiene esa ventana y es
  retirable en cuanto cae.

Es decir: **crear envíos gasta contra `balance_cents`; retirar gasta contra
`available_cents`.** Una parte pequeña del efectivo es la excepción de la
primera mitad.

Cada respuesta trae también `in_review_cents`. Cuando surge una duda sobre un
pedido en efectivo mientras liquidamos la semana, el efectivo de ese pedido se
queda retenido mientras lo revisamos, y este es el monto de `balance_cents` que
eso representa. Está dentro del efectivo retenido de arriba, así que nunca entra
en `available_cents` y no se puede adelantar, y a diferencia del resto del
efectivo retenido **no** se puede gastar en envíos: crear un envío responde `402
insufficient_balance` cuando `balance_cents` menos `in_review_cents` no alcanza.
Es `0` en casi todas las cuentas, no trae fecha y se libera cuando tenemos una
respuesta.

Vale la pena planearlo: una cuenta fondeada solo con tarjeta se puede gastar
completa y **no se puede retirar nada** hasta que venza la primera retención,
seis meses después. Si piensas retirar, fondea por SPEI.

```ruby balance_and_withdrawal.rb theme={null}
# Reads the balance deliveries are paid from, then moves part of it to the bank
# account on file.
#
#   LISTO_API_KEY=lst_test_... ruby balance_and_withdrawal.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")

# 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 request(verb, path, payload = nil)
  uri = URI("#{BASE_URL}#{path}")

  request = verb.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"

  if payload
    request["Content-Type"] = "application/json"
    request.body = JSON.generate(payload)
  end

  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.
  abort("#{path} refused: #{parse_error(response)["code"]}") unless response.code.start_with?("2")

  JSON.parse(response.body)
end

# `balance_cents` is everything the account holds. `available_cents` is the part
# that can leave it today: cash a courier collected is on the balance from the
# moment it is delivered, but it is only withdrawable once it has been settled.
# Deliveries spend against the balance, withdrawals against the available part.
balance = request(Net::HTTP::Get, "/v1/balance")

puts "balance #{balance["balance_cents"]} #{balance["currency"]}"
puts "available #{balance["available_cents"]}"

# Asking for more than is available is refused with `insufficient_balance`, so
# ask for what the last read said was there.
withdrawal = request(
  Net::HTTP::Post,
  "/v1/withdrawals",
  {"amount_cents" => balance["available_cents"]}
)

# The money is reserved the moment this returns: it leaves `available_cents`
# straight away and the transfer follows. You learn the outcome from the
# `withdrawal.updated` event, or by reading the withdrawal back.
puts "withdrawal #{withdrawal["id"]} is #{withdrawal["status"]}"
```

## Todos los movimientos, en orden

[`GET /v1/ledger`](https://docs.listo.delivery/es/api-reference/ledger/list-your-ledger-entries) es el
historial detrás de esos dos números: una fila por
movimiento de la cuenta, del más reciente al más viejo, en los mismos centavos
con signo. Los abonos son positivos y los cargos negativos, así que las filas se
suman sin tener que deducir la dirección a partir del tipo.

```json theme={null}
{
  "data": [
    {
      "id": "le_6nQ4vX8mK2dR7tYb3nZ1sLwC",
      "type": "provider_charge",
      "amount_cents": -18560,
      "delivery_id": "dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe",
      "store_id": "st_4bR8kM2wQ7xT1nV5cZ9pLdYe",
      "test": false,
      "occurred_at": "2026-08-01T18:41:03Z"
    },
    {
      "id": "le_2xN6bK9mR4tQ7vC1sZ8pLwYe",
      "type": "subsidy_refund",
      "amount_cents": 9280,
      "delivery_id": "dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe",
      "store_id": "st_4bR8kM2wQ7xT1nV5cZ9pLdYe",
      "test": false,
      "occurred_at": "2026-08-01T18:40:58Z"
    }
  ],
  "has_more": true,
  "cursor": "1785782458000000-4127"
}
```

Esas dos filas son justo el par que describe
[el modelo de dinero](https://docs.listo.delivery/es/concepts/money.md): un envío devuelto regresó
completa la parte del negocio y cobró los dos tramos que se manejaron, como dos
movimientos y no como un reembolso encogido.

Cada fila dice de dónde salió:

| `type` | Qué es |
| - | - |
| `topup_card`, `topup_spei` | Dinero que metiste, con tarjeta o por transferencia |
| `subsidy` | Tu parte de una tarifa de envío, cargada al crear el envío |
| `subsidy_refund` | Esa parte de regreso, en un envío que no llegó |
| `provider_charge` | La tarifa de envío cobrada por una cancelación tardía o por un viaje de devolución, una por cada tramo manejado |
| `provider_charge_reversal` | Uno de esos cargos abonado de regreso completo, cuando resulta que el costo no se debía |
| `requote_adjustment` | La diferencia entre la tarifa que se le cobró a un pedido y la que la red de repartidores reservó para su viaje, cuando difieren. Negativa si el viaje reservado costó más, positiva si costó menos |
| `cash_earning` | El valor de un pedido en efectivo ya entregado |
| `partner_markup` | Tu propio margen en un pedido entregado, abonado a ti |
| `developer_share` | Lo que ganas en un viaje entregado de tu propia tienda, abonado a ti |
| `plan_charge` | Un mes del Plan Listo de una tienda en modo real, IVA incluido, cuando lo pagó tu saldo. Ve [plan y cobro](https://docs.listo.delivery/es/guides/plan-and-billing.md) |
| `plan_extra_charge` | Conversaciones del asistente más allá de las que incluye el plan de una tienda, cobradas junto con ese plan |
| `withdrawal` | Dinero enviado a tu banco |
| `withdrawal_fee` | La comisión fija de un retiro el mismo día, IVA incluido, como su propia fila junto al retiro |
| `adjustment` | Una corrección que registra Listo, en cualquier dirección |

Esta lista crece. Aparecen valores nuevos de `type` en cualquier versión, porque
los cambios a v1 son aditivos y un movimiento que todavía no nombramos sigue
siendo dinero que se movió. Haz tu lógica sobre los valores que conoces y deja
pasar el resto: cuenta un `type` desconocido dentro del saldo, muéstralo, y no lo
descartes ni falles con él. Las filas que tires son justo la diferencia entre tus
libros y `GET /v1/balance`.

> [!WARNING]
>
> **Los dos campos type son vocabularios distintos**
>
> El `charges[].type` de un envío dice de qué fue el cargo: `cancellation_fee` o
> `return_fee`. El `type` de una fila del historial dice de dónde salió el
> movimiento, y todos esos cargos llegan bajo un solo valor: `provider_charge`.
>
> Es el mismo dinero visto desde dos lados. El envío te dice qué se te cobró por
> ese pedido; el historial te dice hacia dónde se movió en la cuenta, que es por lo
> que el monto es positivo en uno y negativo en el otro. No mapees un vocabulario
> sobre el otro, y no esperes ver `return_fee` como `type` de una fila.
>
> Empata los dos por `delivery_id` y nunca por id. El id lo tiene solo la fila del
> historial; el cargo del envío no tiene, porque su manija es el envío mismo.

Una fila que viene de uno de tus pedidos trae su `delivery_id`, que es como se le
atribuye un costo a un pedido. Eso incluye los pedidos que tus tiendas
despacharon desde su propia consola de Listo: también son pedidos tuyos y traen
el mismo id `dlv_`, sin `external_id` porque tú nunca lo enviaste. `null`
significa que el movimiento no pertenece a ningún pedido: una recarga, un retiro,
el plan de una tienda o un ajuste. `test` dice a qué mundo pertenece la fila,
aunque una llave solo ve el suyo.

`store_id` dice a qué tienda pertenece un movimiento, y viene en cada fila: la
tienda del pedido en una fila que viene de uno, la tienda cuyo plan se pagó en un
`plan_charge`. Viene en `null` cuando el movimiento no pertenece a una sola
tienda, como una recarga o un retiro de tu propio saldo.

Dos filtros, y responden las dos preguntas que vale la pena hacer:

* `GET /v1/ledger?delivery_id=dlv_...` es todo lo que se movió por un solo
  pedido. Esto es lo que le facturas a tu propio comercio.
* `GET /v1/ledger?type=provider_charge` es cada cargo por viaje desperdiciado
  que se te ha cobrado, de todos los pedidos.

Los dos prefieren rechazar antes que confundirte. Un `type` que no reconocemos no
empata con nada y devuelve una página vacía, en vez de devolverte calladamente
todo, y un `delivery_id` que no es un pedido tuyo en este modo devuelve
`404 not_found` y no una página vacía, porque una página vacía es una respuesta
verdadera a "ese pedido no costó nada extra" y se leería como tal.

Cada uno recibe exactamente un valor. No hay lista separada por comas ni llave
repetida, y mandar cualquiera de las dos no amplía el filtro: es un valor que no
empata con nada, así que `type` regresa vacío y `delivery_id` se rechaza como
cualquier otro id que no sea tuyo.

Se pagina como cualquier otro listado de aquí: hasta 50 filas, un booleano
`has_more` y un `cursor` opaco que regresas para pedir la siguiente página. El
cursor es `null` en la última página, así que nunca puede contradecir a
`has_more` sobre si vale la pena pedir otra.

Solo camina hacia atrás, hacia el historial. Para agarrar los movimientos que se
registraron desde tu último recorrido, vuelve a leer la primera página en lugar
de reenviar el cursor con el que terminaste.

## Cómo cobrarle un costo al pedido que lo causó

Esta es la conciliación para la que existe el historial: recorres cada cargo por
viaje desperdiciado y le cobras cada uno al pedido que lo provocó.

```ruby reconcile_charges.rb theme={null}
# Walks every wasted-trip charge on your balance, so each one can be billed to
# the order that caused it. A cancellation once a courier was already working,
# or a return trip after a failed dropoff, is billed at the delivery fee for
# each leg that was driven and lands on the ledger as a `provider_charge`.
#
#   LISTO_API_KEY=lst_test_... ruby reconcile_charges.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")

# 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 get(path, query)
  uri = URI("#{BASE_URL}#{path}")
  uri.query = URI.encode_www_form(query)

  request = Net::HTTP::Get.new(uri)
  request["Authorization"] = "Bearer #{API_KEY}"

  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.
  abort("GET #{path} refused: #{parse_error(response)["code"]}") unless response.code == "200"

  JSON.parse(response.body)
end

# Reconcile from the ledger rather than from the events you received. Events are
# a notification that something happened, at least once and in no order; this is
# the money itself, once each and in order. It is also the only place a cost
# that posted after an order finished is guaranteed to be waiting for you.
#
# `type` narrows the ledger to those charges. Every other movement,
# your share of a delivery fee and its refund, cash earnings, top-ups and
# withdrawals, stays out of this walk.
cursor = nil
count = 0
total = 0

loop do
  query = {"type" => "provider_charge"}
  query["cursor"] = cursor if cursor

  page = get("/v1/ledger", query)

  page["data"].each do |entry|
    count += 1

    # SIGNED cents, so a charge is negative because it left your balance.
    # Adding the rows up is the whole sum; taking an absolute value here would
    # turn every charge into a credit.
    total += entry["amount_cents"]

    # The order to bill this to. Match it against the id you stored when you
    # created the delivery, or read the order back with
    # GET /v1/deliveries/<id> for your own `external_id`.
    #
    # Every wasted-trip charge names its order, including the orders your
    # stores dispatched from their own Listo console. Those carry no
    # `external_id`, because you never sent one, so read them back by their
    # `dlv_` id.
    #
    # Null means the movement belongs to no order at all: a top-up, a
    # withdrawal or an adjustment. It cannot reach this walk while the `type`
    # filter is on, and the fallback is here for when you widen it. Bill nobody
    # for such a row rather than attributing it to whichever order came next.
    order = entry["delivery_id"] || "(no order)"

    puts "#{order} #{entry["amount_cents"]}"
  end

  # Every list on this API answers the same shape: `data`, `has_more`, and an
  # opaque `cursor`. Keep passing the cursor back until `has_more` is false. A
  # walk that reads one page and stops would bill only your most recent orders.
  break unless page["has_more"]

  cursor = page["cursor"]
end

puts "charges #{count}"
puts "total #{total}"
```

```
dlv_2xN6bK9mR4tQ7vC1sZ8pLwYe -9280
dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe -18560
charges 2
total -27840
```

Ahí hay tres cosas que conviene copiar en lugar de reescribir. El ciclo sigue
regresando el `cursor` hasta que `has_more` sea falso, porque un recorrido que
lee una sola página nada más le cobra a tus pedidos más recientes. Los montos se
suman tal como llegan, porque un cargo es negativo y un valor absoluto
convertiría cada cargo en un abono. Y una fila cuyo `delivery_id` es `null` se
señala en vez de atribuirse, porque adivinar a qué pedido pertenecía es la forma
de cobrarle a un comercio el viaje de alguien más.

> [!TIP]
>
> **Concilia con el historial, no con tus eventos**
>
> Los webhooks son un aviso de que algo pasó. El historial es el registro de lo que
> pasó, así que un evento perdido, uno reenviado o un endpoint caído una hora no
> cambian nada de lo que puedes rearmar después. Recorre el historial del periodo
> que estás cerrando y ahí está cada movimiento, en orden y una sola vez.

## El saldo de una sola tienda

Todo lo de arriba es tu cuenta. Una tienda puede ser dueña de su dinero, que es
lo que decide el `access_tier` del objeto de tienda. En
[el modelo de dinero](https://docs.listo.delivery/es/concepts/money.md) está qué significa ese arreglo;
aquí está cómo leerlo.

Dos endpoints responden por una tienda, y responden por **todas** las tiendas y
no solo por las que son dueñas de su dinero:

* [`GET /v1/stores/{id}/balance`](https://docs.listo.delivery/es/api-reference/balance/read-a-stores-balance)
* [`GET /v1/stores/{id}/ledger`](https://docs.listo.delivery/es/api-reference/ledger/list-a-stores-ledger-entries)

```json theme={null}
{
  "balance_cents": 84200,
  "available_cents": 41100,
  "currency": "MXN",
  "billing": "store"
}
```

Los primeros tres campos significan exactamente lo mismo que en
`GET /v1/balance`. El cuarto dice de quién es el saldo que estás viendo:
`"store"` es el de la tienda, `"partner"` es el tuyo, que es lo que reporta una
tienda en el tier por defecto porque ese sí es el saldo del que gastan sus
envíos. Dos tiendas que gastan de tu saldo reportan la misma cifra, y es la misma
que da `GET /v1/balance`.

El historial es la misma ventana con la misma forma: los mismos campos por fila,
el mismo vocabulario de `type`, las mismas páginas de 50 filas y el mismo cursor
opaco, y los dos filtros funcionan igual. Lo que hayas escrito para recorrer
`GET /v1/ledger` recorre este sin cambiarle nada.

> [!NOTE]
>
> **Una tienda que gasta de tu saldo muestra tus movimientos**
>
> En una tienda cuyo `billing` es `"partner"`, estos dos endpoints están leyendo tu
> cuenta, así que las filas incluyen movimientos de tus otras tiendas. Esa es la
> respuesta honesta a "qué se ha movido en el saldo del que gasta esta tienda", y
> son los mismos datos que `GET /v1/ledger` ya le da a la misma llave.
>
> `?delivery_id=` sigue la misma regla: nombra cualquier pedido tuyo en este modo,
> no solo los de esta tienda. Uno que se cobró a otra cuenta regresa como una
> página vacía, y uno que no es tuyo es `404 not_found`.

## Cómo darle acceso al dueño de una tienda

Una tienda dueña de su dinero necesita a alguien que pueda verlo, así que
[`POST /v1/stores/{id}/owner`](https://docs.listo.delivery/es/api-reference/stores/invite-a-store-owner)
crea el acceso de esa persona a Listo.

```json theme={null}
{ "email": "rosa@tacoslamera.mx", "name": "Rosa Delgado" }
```

```json theme={null}
{ "email": "rosa@tacoslamera.mx", "invited_at": "2026-08-01T12:41:03Z" }
```

Listo le manda por correo una liga para que ella elija su contraseña. Tú nunca
ves una contraseña ni un token, y no hay nada que guardar de la respuesta: la
credencial existe solo a partir de que esa persona la eligió.

Llama otra vez y la misma persona recibe una liga nueva en lugar de un segundo
acceso, así que reintentar tras un timeout es seguro, y también lo es reenviar la
liga cuando alguien dice que la primera ya venció. La liga dura
siete días.

Que la dirección ya tenga un acceso a Listo en otro lado no es problema. Esa
persona recibe un acceso aparte para esta tienda, con su propia contraseña, y la
cuenta que ya tenía queda como estaba. Alguien que lleva dos de tus sucursales
puede usar la misma dirección en las dos.

Tres rechazos:

* `422 console_access_required` en una tienda cuyo `access_tier` es `api_only`.
  Esa tienda no tiene consola a la cual entrar, así que no hay a qué invitar a
  nadie. Cambia el tier primero y vuelve a llamar.
* `422 validation_failed` con `param: "email"` cuando la tienda ya tiene un dueño
  con otra dirección. El `message` dice quién es. Una tienda tiene un solo dueño
  y este endpoint no puede cambiarlo. Para mandarle una liga nueva a ese dueño,
  llama con la dirección que aparece en el `message`.
* `422 validation_failed` con `param: "email"` cuando otra petición está dando
  de alta esa misma dirección para esta tienda en ese mismo momento. Repite la
  llamada: para entonces la primera ya terminó y la tuya reenvía la liga.

## Cómo fondearlo

Las recargas no son una llamada a la API. Fondea el saldo desde el
[panel de socios](https://partners.listo.delivery), con tarjeta o transfiriendo
a la CLABE permanente que ahí aparece. Esa CLABE no cambia, así que la puedes
guardar como beneficiario sin problema.

Las dos rutas se diferencian en más que la comodidad, y vale la pena elegir:

| | Cuánto cuesta | Retirable |
| - | - | - |
| **Transferencia SPEI** | \*\*MX$8.12**, descontados de lo que mandas. Transfieres MX$5,000 y al saldo llegan MX\$4,991.88 | De inmediato |
| **Tarjeta** | Se suma **por encima**, para que el abono llegue completo. Abonar MX$2,000 te cuesta MX$2,090.79 | A los 180 días |

La comisión de tarjeta es 3.6% más MX$3.00, ambos más IVA, calculados hacia
arriba para que el monto que pediste abonar sea el que llega, y no el monto al
que se le descontó la comisión. La recarga mínima con tarjeta es de MX$10.

Deja un colchón. Una creación cuya parte del negocio no alcanza a cubrir el
saldo se rechaza con `402 insufficient_balance` y no se despacha nada, así que
un saldo vacío detiene pedidos nuevos.

Lo que no detiene es todo, y esta es la única forma en que un saldo se va a
negativo. El cargo por una cancelación tardía o por una devolución se registra
alcance o no alcance el saldo, porque ese viaje ya se manejó y no registrarlo no
lo desharía. Así que un pedido cancelado cuando ya hay un
repartidor encima, o uno que se regresa tras una entrega fallida, te puede dejar
abajo de cero hasta que recargues, y un saldo negativo bloquea la siguiente
creación.

Es más acotado de lo que suena: no se cobra nada si no hubo un repartidor
realmente comprometido, y un pedido que terminó en `no_courier` no cuesta.

Lo que sí se cobra todavía puede regresar. Un viaje cancelado por el repartidor,
o por una falla del lado de Listo, es un costo que reclamamos por tu cuenta
después del hecho. Cuando eso prospera, llega como un `provider_charge_reversal`:
el mismo monto, abonado completo, contra el mismo pedido. Es una fila aparte y no un
encogimiento retroactivo del cargo, así que siempre puedes ver tanto lo que se te
cobró como lo que regresó, y puede registrarse semanas después del cargo. No
dejes una factura abierta esperándolo, ni des por hecho que un cargo se va a
revertir. Cobra el cargo, y traslada el abono a tu propio comercio si llega y
cuando llegue.

Un saldo que bajó sin que se creara un envío siempre tiene explicación.
`GET /v1/ledger?type=provider_charge` lista cada uno de esos cargos con el pedido
del que salió, y `?type=provider_charge_reversal` lista los que regresaron.

> [!TIP]
>
> **Vigila el saldo, no los errores**
>
> Lee `GET /v1/balance` de forma periódica y alerta contra tu propio piso, en vez
> de esperar a la primera creación rechazada. Un 402 llega en el peor momento
> posible: el cliente ya hizo su pedido.

## Cómo retirar

`POST /v1/withdrawals` con un `amount_cents`. El dinero queda apartado en cuanto
la llamada responde: sale de `available_cents` de inmediato, así que un segundo
retiro no puede gastarlo también.

```json theme={null}
{
  "id": "wd_2xN6bK9mR4tQ7vC1sZ8pLwYe",
  "amount_cents": 250000,
  "fee_cents": 0,
  "currency": "MXN",
  "kind": "standard",
  "status": "pending",
  "test": false,
  "requested_at": "2026-07-30T18:30:00Z",
  "due_at": null,
  "resolved_at": null,
  "clave_de_rastreo": null
}
```

Se queda en `pending` mientras se gestiona la transferencia, y luego se resuelve
en `paid` o, si la transferencia rebota, en `failed`, lo cual regresa el monto
apartado a tu saldo disponible. Te enteras de cuál por el webhook
`withdrawal.updated`, o leyéndolo de vuelta con
[`GET /v1/withdrawals/{id}`](https://docs.listo.delivery/es/api-reference/withdrawals/fetch-a-withdrawal).

## Cómo recibirlo el mismo día

`kind` es opcional y vale `standard` cuando lo omites, que es todo lo anterior:
solo tu saldo ya liberado, sin comisión, enviado en 1 a 3 días hábiles. Nada de
ese camino cambió, así que una integración escrita antes de que esto existiera
sigue pidiendo exactamente lo que siempre pidió.

Manda `"kind": "same_day"` y la transferencia sale el mismo día.

```json theme={null}
{
  "id": "wd_2xN6bK9mR4tQ7vC1sZ8pLwYe",
  "amount_cents": 409000,
  "fee_cents": 3000,
  "currency": "MXN",
  "kind": "same_day",
  "status": "pending",
  "test": false,
  "requested_at": "2026-08-20T20:20:00Z",
  "due_at": "2026-08-20T23:20:00Z",
  "resolved_at": null,
  "clave_de_rastreo": null
}
```

**Cuesta una comisión fija, IVA incluido**, que la respuesta reporta en
`fee_cents`. La comisión es un SEGUNDO cargo y no un descuento, así que tu banco
recibe completo el `amount_cents` y tu saldo pierde los dos juntos. Concilia un
depósito contra `amount_cents` en los dos tipos; concilia tu saldo contra la
suma. `fee_cents` es `0` en todo retiro `standard`, así que puedes sumar los dos
sin ramificar.

**Alcanza el efectivo que todavía está en espera**, cosa que el camino sin costo
no puede, así que aquí el límite no es `available_cents`. Lo que puede tomar es
un tope que calculamos por cuenta y que ya viene con la comisión descontada, y
ese tope lo puedes leer antes de pedir: viene en `GET /v1/balance`.

**Sale el mismo día**, entre las 8 am y las 8 pm del centro de México. Puedes
pedirlo a cualquier hora: una solicitud hecha dentro de ese horario vence tres
horas después, y una hecha fuera sale a la mañana siguiente. `due_at` es la
fecha límite que tu solicitud realmente lleva, sellada cuando la hiciste y nunca
recalculada, para que puedas exigirnos la promesa que te dimos y no una regla
que pudo haber cambiado después. Es null en todo retiro `standard`, porque el
camino sin costo promete un rango de días hábiles y no un instante.

**Hay un mínimo por solicitud y un máximo diario por cuenta**, y cada rechazo de
abajo nombra su propio límite en `message`.

Tres rechazos pertenecen a este camino:

| Estado y código | Cuándo |
| - | - |
| `402 insufficient_balance` | el monto más su comisión es más de lo que se puede retirar hoy |
| `422 same_day_cap_exceeded` | el máximo del día ya no tiene espacio para él |
| `422 same_day_below_minimum` | el monto está por debajo del mínimo |

Solo el primero se trata de falta de dinero, y por eso los otros dos no son
402: a quien rechaza el máximo del día puede sobrarle saldo, y a quien rechaza
el mínimo le sobra también, solo que está pidiendo muy poco.
`same_day_cap_exceeded` pone `param` en `amount_cents` mientras quede espacio en
el día y lo deja en null cuando el día ya se agotó, porque en ese estado ningún
monto pasa antes de mañana por la mañana. `same_day_below_minimum` siempre lo
pone, porque un número más grande siempre funciona.

> [!WARNING]
>
> **Un kind que no reconocemos se rechaza**
>
> Un `kind` que no sea ninguna de las dos palabras responde
> `422 validation_failed` con `param: "kind"`. Nunca cae de regreso al camino sin
> costo: recibir la transferencia lenta y gratuita cuando pediste una del mismo
> día, sin que nada lo diga, es peor que un error. Un `null` explícito SÍ es el
> camino sin costo, porque omitir el campo y mandar null significan lo mismo.

## Cómo leer la oferta antes de pedir

`GET /v1/balance` trae un objeto `same_day` junto a sus tres cifras. Describe la
OFERTA y no un cuarto saldo, así que puedes dibujar la opción en lugar de
descubrirla en un rechazo.

```json theme={null}
"same_day": {
  "fee_cents": 3000,
  "minimum_cents": 10000,
  "ceiling_cents": 397000,
  "reach_cents": 400000,
  "advanceable_cents": 131600,
  "cap_cents": 500000,
  "cap_used_cents": 0,
  "cap_remaining_cents": 500000,
  "window_open": true,
  "due_at": "2026-08-20T23:20:00Z",
  "next_window_opens_at": "2026-08-21T14:00:00Z",
  "offerable": true,
  "unavailable_reason": null
}
```

`ceiling_cents` es el `amount_cents` más grande que aceptaríamos ahora mismo en
un retiro del mismo día. **No es otro nombre para `available_cents`.** El tope
ya trae la comisión descontada, ya está limitado por lo que queda del día y
alcanza efectivo en espera que `available_cents` deja fuera por definición. En
una cuenta con efectivo cobrado en la puerta es el número mayor; en una cuenta
cuyos cargos rebasaron el saldo es el menor, o cero. Lee el que responde tu
pregunta y nunca lo deduzcas del otro.

`reach_cents` es todo lo que el retiro podría tomar antes de la comisión, y
`advanceable_cents` es la parte de eso que sigue en espera. `minimum_cents` es
el mínimo por solicitud, y `cap_cents`, `cap_used_cents` y `cap_remaining_cents`
son el máximo diario por cuenta y en dónde vas hoy contra él, para que un tope
chico junto a un saldo grande se lea como un límite y no como una falla.

`offerable` es falso cuando ninguna solicitud del mismo día sería aceptada, y
`unavailable_reason` dice en cuál de los dos estados estás: `cap_reached`, que
se libera solo mañana por la mañana, o `below_minimum`, que no se libera hasta
que entre más dinero. Son los mismos dos estados detrás de
`same_day_cap_exceeded` y `same_day_below_minimum`, así que puedes mostrar el
motivo sin gastar una solicitud en averiguarlo. Es null siempre que `offerable`
sea verdadero.

`window_open` dice si estamos enviando en este momento. `due_at` es la fecha
límite que cargaría una solicitud hecha en ese instante y
`next_window_opens_at` es cuándo se reanudan los envíos. La fecha límite que tu
solicitud realmente carga es el `due_at` del retiro que regresa, sellado en ese
momento y nunca recalculado.

## Cómo casar un pago con tu estado de cuenta

`clave_de_rastreo` es la clave de rastreo SPEI de la transferencia, el
identificador con el que tu propio banco localiza el depósito. Es con lo que se
concilia contra un estado de cuenta: el monto se repite entre solicitudes,
`requested_at` es nuestro reloj y no el del banco, y el id `wd_` no significa
nada fuera de Listo.

Siempre viene en el objeto y es null hasta que la transferencia se envía de
verdad, así que llega junto con el estado `paid` en el webhook
`withdrawal.updated` y en cualquier lectura posterior. En un retiro `failed` se
queda en null, porque nunca hubo transferencia.

```json theme={null}
{
  "id": "wd_2xN6bK9mR4tQ7vC1sZ8pLwYe",
  "status": "paid",
  "resolved_at": "2026-08-01T16:45:00Z",
  "clave_de_rastreo": "MBAN01002608120001234567"
}
```

Es la misma clave que aparece en ese retiro en el
[panel](https://partners.listo.delivery), así que las dos nunca se contradicen.
El comprobante de pago que el equipo adjunta vive solo en el panel: esta API
publica la clave, no el documento.

Pedir más de `available_cents` devuelve `402 insufficient_balance` y no aparta
nada.

No hay verbo para cancelar ni modificar un retiro. Una vez solicitado termina
pagado o fallido, y los dos desenlaces se te avisan.

La transferencia se hace a la cuenta bancaria registrada, que configuras en el
panel. Regístrala antes de solicitar tu primer retiro: sin ella esta llamada
responde `422 payout_account_not_ready` y no aparta nada.

El mismo rechazo cubre el día siguiente a que CAMBIES esa cuenta. Un destino
recién movido no puede recibir dinero durante 24 horas, que es la ventana en la
que todavía se puede detener a alguien que no seas tú moviéndolo, y el `message`
del rechazo dice el instante exacto en que vuelve a poder. Esperar es el único
remedio: volver a guardar la misma cuenta reinicia la ventana.

## A dónde va el dinero de las tarjetas

Los pedidos con tarjeta no se liquidan por este saldo en absoluto. Lo que el
cliente paga por la mercancía se va directo a quien figure como comerciante de
registro de esa tienda, la parte de Listo de la tarifa de envío se toma de ese
mismo cargo, y nada de eso aparece en `balance_cents`.

El único movimiento de un pedido con tarjeta que vas a ver en el saldo es tu
propia parte de la tarifa de envío, cargada al crear como en cualquier otro
pedido.


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