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.
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.
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.
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_chargeis every wasted-trip charge you have been billed, across all orders.
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
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.
One store’s balance
Everything above is your account. A store can own its money instead, which is whataccess_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:
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, soPOST /v1/stores/{id}/owner
provisions that person’s Listo login.
422 console_access_requiredon a store whoseaccess_tierisapi_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_failedwithparam: "email"when the store already has an owner at a different address. Themessagenames 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 themessagenames.422 validation_failedwithparam: "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.
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.
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.
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.
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.
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 inbalance_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.