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

# Fund your balance and withdraw

> Where the money for a delivery comes from, and how to take it out.

Your account holds one balance per mode. Deliveries are paid out of it, cash
your couriers collect is paid into it, and withdrawals move what is left to your
bank. This page is the operational half; what each movement *means* is
[the money model](https://docs.listo.delivery/en/concepts/money.md).

## Two numbers

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

`balance_cents` is everything the account holds. `available_cents` is the part
that can leave it today.

Money is held back from `available_cents` for two reasons, and in both cases it
is fully spendable on deliveries while it waits:

* **Cash your couriers collected.** When a cash order is delivered, its order
  value is credited to your balance immediately. It becomes withdrawable once
  that cash has actually been settled to Listo, on a weekly cycle.
* **Money you added by card.** A card top-up is held for **180 days** from the
  charge, the window in which a card payment can still be disputed. A SPEI
  transfer carries no such window and is withdrawable the moment it lands.

So: **creating deliveries spends against `balance_cents`; withdrawing spends
against `available_cents`.** One small part of the cash is the exception to the
first half.

Every response also carries `in_review_cents`. When a question comes up about a
cash order while we settle the week, that order's cash stays held while we look
into it, and this is how much of `balance_cents` that is. It sits inside the
held cash above, so it is never in `available_cents` and can never be advanced,
and unlike the rest of the held cash it is **not** spendable on deliveries
either: a create answers `402 insufficient_balance` when `balance_cents` minus
`in_review_cents` falls short. It is `0` on nearly every account, it carries no
date, and it clears when we have an answer.

Worth planning around: an account funded entirely by card is fully spendable and
**not withdrawable at all** until its first hold expires six months later. If
you intend to withdraw, fund by 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"]}"
```

## Every movement, in order

[`GET /v1/ledger`](https://docs.listo.delivery/api-reference/ledger/list-your-ledger-entries) is the
history behind those two numbers: one row per movement
on the account, newest first, in the same signed cents. Credits are positive,
debits are negative, so the rows add up without having to infer a direction from
the type.

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

Those two rows are the pair described in
[the money model](https://docs.listo.delivery/en/concepts/money.md): a returned delivery gave back the
whole business share and billed the two legs that were driven, as two movements
rather than one shrunken refund.

Every row says where it came from:

| `type` | What it is |
| - | - |
| `topup_card`, `topup_spei` | Money you added, by card or by transfer |
| `subsidy` | Your share of a delivery fee, debited when the delivery was created |
| `subsidy_refund` | That share coming back, on a delivery that never arrived |
| `provider_charge` | The delivery fee billed for a late cancellation or a return trip, one per leg driven |
| `provider_charge_reversal` | One of those charges credited back in full, when the cost turns out not to have been owed |
| `requote_adjustment` | The difference between the fee an order was charged and the fee the courier network booked for its trip, when the two differ. Negative when the booked trip cost more, positive when it cost less |
| `cash_earning` | The order value of a delivered cash order |
| `partner_markup` | Your own markup on a delivered order, credited to you |
| `developer_share` | What you earn on a delivered trip of your own store, credited to you |
| `plan_charge` | One month of one live store's Plan Listo, IVA included, when your balance paid it. See [plan and billing](https://docs.listo.delivery/en/guides/plan-and-billing.md) |
| `plan_extra_charge` | Assistant conversations past what a store's plan includes, charged with that store's plan |
| `withdrawal` | Money sent to your bank |
| `withdrawal_fee` | The flat fee for a same-day withdrawal, IVA included, as its own row beside the withdrawal |
| `adjustment` | A correction posted by Listo, in either direction |

This list grows. New `type` values ship in ordinary releases, because changes to
v1 are additive and a movement we have not named before is still money that
moved. Branch on the values you know and let the rest fall through: count an
unfamiliar `type` toward the balance, show it, and do not drop it or fail on it.
The rows you discard are exactly the difference between your books and
`GET /v1/balance`.

> [!WARNING]
>
> **The two type fields are different vocabularies**
>
> A delivery's `charges[].type` says what the charge was for: `cancellation_fee`
> or `return_fee`. A ledger row's `type` says where the movement came from, and
> every one of those charges arrives under the single value `provider_charge`.
>
> It is the same money seen from two sides. The delivery tells you what you were
> charged for that order; the ledger tells you which way it moved on the account,
> which is why the amount is positive on the one and negative on the other. Do not
> map either vocabulary onto the other, and do not expect `return_fee` to appear
> as a ledger `type`.
>
> Match the two by `delivery_id` and never by id. Only the ledger row has an id;
> the charge on the delivery has none, because the delivery is its handle.

A row that came from one of your orders carries its `delivery_id`, which is how
a cost gets attributed to an order. That includes the orders your stores
dispatched from their own Listo console: they are your orders too, and they
carry the same `dlv_` id, with no `external_id` because you never sent one.
`null` means the movement belongs to no order at all: a top-up, a withdrawal, a
store's plan or an adjustment. `test` says which world the row belongs to,
though a key only ever sees its own.

`store_id` names the store a movement belongs to, and it is on every row: the
store of the order for a row that came from one, the store whose plan was paid
for a `plan_charge`. It is `null` when the movement belongs to no single store,
such as a top-up or a withdrawal on your own balance.

Two filters, and they answer the two questions worth asking:

* `GET /v1/ledger?delivery_id=dlv_...` is everything that moved because of one
  order. This is what you bill your own merchant from.
* `GET /v1/ledger?type=provider_charge` is every wasted-trip charge you have
  been billed, across all orders.

Both refuse rather than mislead. An unrecognised `type` matches nothing and
returns an empty page instead of quietly returning everything, and a
`delivery_id` that is not one of your orders in this mode is `404 not_found`
rather than an empty page, because an empty page is a truthful answer to "that
order cost nothing extra" and would be read as one.

Each takes exactly one value. There is no comma list and no repeated key, and
sending either is not a wider filter: it is a value nothing matches, so `type`
comes back empty and `delivery_id` is refused like any other id you do not have.

It pages like every other list here: up to 50 rows, a `has_more` boolean and an
opaque `cursor` you send back for the next page. The cursor is `null` on the
last page, so it can never disagree with `has_more` about whether another
request is worth making.

It only ever walks backwards into history. To pick up movements that posted
since your last walk, read the first page again rather than re-sending the
cursor you finished on.

## Billing a cost back to its order

This is the reconciliation the ledger exists for: walk every wasted-trip
charge, and bill each one to the order that caused it.

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

Three things in there are worth copying rather than rewriting. The loop keeps
handing the `cursor` back until `has_more` is false, because a walk that reads
one page bills only your most recent orders. The amounts are added up exactly as
they arrive, since a charge is negative and an absolute value would turn every
charge into a credit. And a row whose `delivery_id` is `null` is called out rather
than attributed, because guessing which order it belonged to is how a merchant
gets billed for somebody else's trip.

> [!TIP]
>
> **Reconcile from the ledger, not from your events**
>
> Webhooks are a notification that something happened. The ledger is the record of
> what happened, so a missed event, a retried one or an endpoint that was down for
> an hour changes nothing about what you can rebuild afterwards. Walk the ledger
> for the period you are closing and you have every movement, in order, once each.

## One store's balance

Everything above is your account. A store can own its money instead, which is
what `access_tier` on the store object decides. See
[the money model](https://docs.listo.delivery/en/concepts/money.md) for what that arrangement means; this
is how you read it.

Two endpoints answer for one store, and they answer for **every** store rather
than only the ones that own their money:

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

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

The first three fields mean exactly what they mean on `GET /v1/balance`. The
fourth says whose balance you are looking at: `"store"` is the store's own,
`"partner"` is yours, which is what a store on the default tier reports because
that really is the balance its deliveries spend. Two stores on your balance both
report the same figure, and it is the same one `GET /v1/balance` gives.

The ledger is the same window in the same shape: the same row fields, the same
`type` vocabulary, the same 50-row pages and the same opaque cursor, and both
filters work the same way. Anything you wrote to walk `GET /v1/ledger` walks
this one unchanged.

> [!NOTE]
>
> **A store on your balance shows your movements**
>
> On a store whose `billing` is `"partner"`, these two endpoints are reading your
> account, so the rows include movements from your other stores. That is the
> truthful answer to "what has moved on the balance this store spends from", and
> it is the same data `GET /v1/ledger` already gives the same key.
>
> `?delivery_id=` follows the same rule: it names any order of yours in this mode,
> not only this store's. One that billed a different account comes back as an
> empty page, and one that is not yours is `404 not_found`.

## Giving a store owner a login

A store that owns its money needs somebody who can see it, so
[`POST /v1/stores/{id}/owner`](https://docs.listo.delivery/api-reference/stores/invite-a-store-owner)
provisions that person's Listo login.

```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 emails them a link to set their own password. You never see a password or
a token, and there is nothing in the response to store: the credential exists
only once that person has chosen it.

Call it again and the same person gets a fresh link rather than a second login,
so a retry after a timeout is safe, and so is sending the link again when
somebody says the first one expired. The link lasts seven days.

An address that already has a Listo login somewhere else is fine. That person
gets a separate login for this store, with its own password, and the account
they already had is left as it was. Somebody who runs two of your locations can
use the same address for both.

Three refusals:

* `422 console_access_required` on a store whose `access_tier` is `api_only`.
  That store has no console to sign in to, so there is nothing to invite anybody
  to. Set the tier first, then call this again.
* `422 validation_failed` with `param: "email"` when the store already has an
  owner at a different address. The `message` names that owner. A store has one
  owner, and this endpoint cannot change who it is. To send the owner a fresh
  link, call it with the address the `message` names.
* `422 validation_failed` with `param: "email"` when another request is setting
  up the same address for this store at the same moment. Repeat the call: by
  then the first one has finished, and yours sends the link again.

## Funding it

Top-ups are not an API call. Fund the balance from the
[partner dashboard](https://partners.listo.delivery), either by card or by
transferring to the permanent CLABE shown there. That CLABE does not change, so
it is safe to save as a payee.

The two routes differ in more than convenience, in ways worth choosing between:

| | What it costs | Withdrawable |
| - | - | - |
| **SPEI transfer** | \*\*MX$8.12**, deducted from what you send. Transfer MX$5,000 and MX\$4,991.88 reaches the balance | Immediately |
| **Card** | Added **on top**, so the credit lands whole. Crediting MX$2,000 costs you MX$2,090.79 | After 180 days |

The card fee is 3.6% plus MX$3.00, both plus IVA, grossed up so that the amount
you asked to credit is the amount that arrives rather than the amount the fee
was taken out of. The minimum card top-up is MX$10.

Keep a buffer. A create whose business share the balance cannot cover is refused
with `402 insufficient_balance` and nothing is dispatched, so an empty balance
stops new orders.

It does not stop everything, and this is the one way a balance goes negative.
The charge for a late cancellation or a return posts whether or not the balance
covers it, because that trip was already driven and refusing to record it would
not un-drive it. So an order cancelled once a courier is genuinely on
it, or one that comes back after a failed dropoff, can leave you below zero
until you top up, and a negative balance then blocks the next create.

It is narrower than it sounds: nothing is charged unless a courier was actually
engaged, and an order that ended at `no_courier` is free.

What is charged can still come back. A trip called off by the courier, or by a
failure on Listo's side, is a cost we take up on your behalf after the fact.
When that succeeds, it lands as a `provider_charge_reversal`: the same amount,
credited back in full, against the same order. It is a separate row rather than a
retroactive shrinking of the charge, so you can always see both what you were
billed and what returned, and it can post weeks after the charge did. Do not
hold an invoice open waiting for one, and do not assume a charge will be
reversed. Bill the charge, and pass the credit back to your own merchant if and
when it arrives.

A balance that fell without a delivery being created is always explainable.
`GET /v1/ledger?type=provider_charge` lists every one of those charges with the
order it came from, and `?type=provider_charge_reversal` lists the ones that
came back.

> [!TIP]
>
> **Watch the balance, not the errors**
>
> Read `GET /v1/balance` on a schedule and alert on your own floor rather than
> waiting for the first refused create. A 402 arrives at the worst possible
> moment: a customer has already ordered.

## Withdrawing

`POST /v1/withdrawals` with an `amount_cents`. The money is reserved the moment
the call returns: it leaves `available_cents` straight away, so a second
withdrawal cannot spend it too.

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

It stays `pending` while the transfer is arranged, then resolves to `paid` or,
if the transfer bounces, `failed`, which releases the reserved amount back to
your available balance. You learn which from the `withdrawal.updated` webhook,
or by reading it back with
[`GET /v1/withdrawals/{id}`](https://docs.listo.delivery/api-reference/withdrawals/fetch-a-withdrawal).

## Getting it the same day

`kind` is optional and `standard` when you leave it out, which is everything
above: your released balance only, no fee, sent in 1 to 3 business days. Nothing
about that path changed, so an integration written before this existed keeps
asking for exactly what it always asked for.

Send `"kind": "same_day"` instead and the transfer goes out the same day.

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

**It costs a flat fee, IVA included**, which the response reports as
`fee_cents`. The fee is a SECOND debit rather than a deduction, so your bank
still receives the whole `amount_cents` and your balance loses the two together.
Reconcile a deposit against `amount_cents` on both kinds; reconcile your balance
against the sum. `fee_cents` is `0` on every `standard` withdrawal, so you can
add the two without branching.

**It reaches cash that is still on hold**, which the free path cannot, so
`available_cents` is not the bound here. What it can take is a ceiling we work
out per account and already net of the fee, and you can read that ceiling before
you ask: it is on `GET /v1/balance`.

**It is sent the same day**, between 8 am and 8 pm Mexico City. You can ask at
any hour: a request made inside those hours is due three hours later, and one
made outside them goes out the following morning. `due_at` is the deadline your
request actually carries, stamped when you made it and never recalculated, so
you can hold us to the promise you were given rather than to a rule that may
have moved since. It is null on every `standard` withdrawal, because the free
path promises a range of business days rather than an instant.

**There is a minimum per request and a daily maximum per account**, and each
refusal below names its own bound in `message`.

Three refusals belong to this path:

| Status and code | When |
| - | - |
| `402 insufficient_balance` | the amount plus its fee is more than can be withdrawn today |
| `422 same_day_cap_exceeded` | the day's maximum has no room for it |
| `422 same_day_below_minimum` | the amount is under the floor |

Only the first is about being short of money, which is why the other two are not
402s: a partner refused by the day's maximum may be sitting on a large balance,
and one refused by the floor is asking for too little. `same_day_cap_exceeded`
sets `param` to `amount_cents` while there is room left on the day and leaves it
null once the day is spent, because in that state no amount succeeds before
tomorrow morning. `same_day_below_minimum` always sets it, because a bigger
number always works.

> [!WARNING]
>
> **A kind we do not recognise is refused**
>
> A `kind` that is neither word is `422 validation_failed` with `param: "kind"`.
> It never falls back to the free path: being given the slow, free transfer when
> you asked for a same-day one, with nothing saying so, is worse than an error.
> An explicit `null` IS the free path, because leaving the field out and sending
> null mean the same thing.

## Reading the offer before you ask

`GET /v1/balance` carries a `same_day` object beside its three figures. It
describes the OFFER rather than a fourth balance, so you can render the choice
instead of discovering it from a refusal.

```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` is the largest `amount_cents` a same-day request would be
accepted for right now. **It is not another name for `available_cents`.** The
ceiling is already net of `fee_cents`, already clamped to what is left of the
day, and it reaches cash on hold that `available_cents` excludes by definition.
On an account sitting on collected cash it is the bigger of the two; on one
whose charges have outrun its balance it is the smaller, or zero. Read the one
that answers your question and never derive it from the other.

`reach_cents` is everything the request could draw on before the fee comes off,
and `advanceable_cents` is the part of that which is still on hold.
`minimum_cents` is the floor per request, and `cap_cents`, `cap_used_cents` and
`cap_remaining_cents` are the daily maximum per account with today's position
against it, so a small ceiling against a large balance is readable as a limit
rather than as a bug.

`offerable` is false when no same-day request would be accepted at all, and
`unavailable_reason` says which state you are in: `cap_reached`, which clears by
itself tomorrow morning, or `below_minimum`, which does not clear until more
money arrives. They are the same two states behind `same_day_cap_exceeded` and
`same_day_below_minimum`, so you can show the reason without spending a request
to learn it. It is null whenever `offerable` is true.

`window_open` says whether we are sending right now. `due_at` is the deadline a
request made at that moment WOULD carry and `next_window_opens_at` is when
sending resumes. The deadline your request actually carries is the `due_at` on
the withdrawal that comes back, stamped then and never recalculated.

## Matching a payout to your bank statement

`clave_de_rastreo` is the SPEI tracking key for the transfer, the identifier
your own bank uses to find the deposit. It is what you reconcile against a bank
statement with: the amount repeats across requests, `requested_at` is our clock
rather than the bank's, and the `wd_` id means nothing outside Listo.

It is always present on the object and null until the transfer is actually
sent, so it arrives with the `paid` status on the `withdrawal.updated` webhook
and on any read after that. On a `failed` withdrawal it stays null, because no
transfer was ever made.

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

It is the same key shown against that withdrawal in the
[dashboard](https://partners.listo.delivery), so the two never disagree. The
payment receipt staff file alongside it lives in the dashboard only; this API
publishes the key, not the document.

Asking for more than `available_cents` returns `402 insufficient_balance` and
reserves nothing.

There is no verb to cancel or amend a withdrawal. Once requested it is either
paid or failed, and both outcomes are told to you.

The transfer goes to the bank account on file, which you set in the dashboard.
Register it before you request your first withdrawal: without one this call is
`422 payout_account_not_ready` and nothing is reserved.

The same refusal covers the day after you CHANGE that account. A moved
destination cannot receive money for 24 hours, which is the window in which
somebody who is not you moving it can still be caught, and the `message` on the
refusal names the instant it can again. Waiting is the only fix: re-saving the
same account starts the window over.

## Where card money goes

Card orders do not settle through this balance at all. What the customer pays
for the goods goes straight to whoever is merchant of record for that store,
Listo's share of the delivery fee is taken from the same charge, and nothing
about it appears in `balance_cents`.

The only card-order movement you will see on the balance is your own share of
the delivery fee, debited at create like every other order.


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