Skip to main content
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.

Two numbers

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.
balance_and_withdrawal.rb

Every movement, in order

GET /v1/ledger 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.
Those two rows are the pair described in the money model: 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: 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.
The two type fields are different vocabulariesA 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.
reconcile_charges.rb
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.
Reconcile from the ledger, not from your eventsWebhooks 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 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:
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.
A store on your balance shows your movementsOn 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 provisions that person’s Listo login.
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, 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: 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.
Watch the balance, not the errorsRead 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.
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}.

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.
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: 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.
A kind we do not recognise is refusedA 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.
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.
It is the same key shown against that withdrawal in the dashboard, 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.