an error's doc_url has no trailing slash, and the documentation moves to docs.listo.delivery
The
doc_url of an error now has no trailing slash: .../en/errors/not_found instead of .../en/errors/not_found/. The documentation answers a page at the address without it, so use the value as it comes. The documentation now lives at https://docs.listo.delivery, and the guide links inside the descriptions of this reference point there; the old addresses under api.listo.delivery/docs answer a permanent redirect to the same page, except the OpenAPI document, which stays at https://api.listo.delivery/docs/openapi.yaml. The description of the bearer authentication scheme now also says to try calls with a test key (no real courier is sent and no real money moves, but webhooks fire and the dropoff.phone you give can receive WhatsApp messages, so use your own number), and that a live key books and charges real deliveries. Nothing else changes.shared types for every response, the webhook events as one union, and what the server enforces
The reference now describes every response and every request body with a named shared type (
Delivery, Store, Quote, Withdrawal, Error and the rest) instead of a copy for each endpoint, so a client generated from it has one Delivery to handle everywhere. The six webhook events, and the events you read from GET /v1/events, POST /v1/events/{event_id}/resend and POST /v1/test_webhooks, are one WebhookEvent union that narrows on type. It also says what the server already sends and checks: a field that can be null is typed that way, the closed lists (payment methods, handoffs, cancellation reasons, access tiers, withdrawal kinds) are published as enums, the limits on a request (a tip up to 100000 centavos, a markup up to 1000000, a fee share from 0 to 100, latitude and longitude ranges, the length of an external_id, of the notes and of a refund reason) are published, pickup_ready_at on a create and a quote is a date-time, and every write operation lists the 403 read_only_key it can answer. The endpoints that take no body (retries, the plan and its cancellation, the card onboarding) no longer list one. Latitude and longitude are published as double precision numbers (format: double), which is how the server sends them, so a generated client does not round a coordinate to 32 bits, and the list of delivery statuses now includes awaiting_location, the status of an order your store took through Listo’s console or WhatsApp assistant while it waits for the customer to confirm the drop-off location. Nothing the API sends or accepts changes.every error links to its page and names the request, find a call by its request id, and read-only keys
Every error response now carries two more fields in
error: doc_url, the page of this documentation that explains that error code and what to do about it, in the language you asked for with Accept-Language, and request_id, Listo’s id for the call, the same value as the X-Request-Id response header. Paste the request_id into a support request. You can also find the call yourself: GET /v1/request_logs has a new request_id filter (an exact match), and the dashboard’s Request logs page has a search box for it. A key can now be issued as read-only: it can call GET and HEAD and nothing else, and any other call with it is refused with the new 403 read_only_key error, so you can hand it to an AI agent or a dashboard that must not move money. Nothing that worked before changes.developer earnings on the ledger, and a store with no plan
The ledger gains a
developer_share type: what you earn on a delivered trip of your own store, credited to you when the trip is delivered. A store of a partner on pricing with no Plan Listo has plan null, because it needs no plan.rehearse a return, a missing courier and a late pickup on a test key
POST /v1/deliveries accepts an optional simulate on a test key: returned brings the order back to the store, no_courier answers the same 409 no_couriers_available a live create does and creates nothing, and delayed_pickup sends a courier who collects the order about 25 minutes after the create. A live key that sends simulate is refused with 422 validation_failed.see your own calls, events and delivery attempts, then resend an event or send a test one
You can now debug an integration yourself.
GET /v1/request_logs and GET /v1/request_logs/{id} list the calls your keys made, with what you sent and what we answered. GET /v1/events and GET /v1/events/{id} list the events Listo emitted for you, and GET /v1/webhook_deliveries lists every attempt to deliver them and what your endpoint answered. POST /v1/events/{id}/resend sends one event to your endpoint again, POST /v1/test_webhooks sends a sample event of any type with "test": true, and four new error codes (webhook_endpoint_not_configured, event_not_resendable, resend_limit_reached and test_webhook_limit_reached) say when one is refused. A resent event is the snapshot from when it happened, so GET /v1/deliveries is still how you recover where things stand now.the return code's description, wording only
The description of
return_verification now says plainly what the code is for and when pincode is set: a 4 digit string while the delivery is returning, null otherwise. Nothing in the API changed.every delivery carries a return code
Every delivery carries
return_verification, whose pincode is the 4 digit return code the store gives the courier to complete a return. The object is always there, and pincode is a 4 digit string while status is returning, and null otherwise.deliveries carry their proofs, and phone numbers are checked as E.164
Every delivery carries
proofs, the evidence the trip left at pickup, drop-off and return, and the links in proof are now Listo’s own 24 hour signed links, null for the few seconds before Listo holds its copy. Phone numbers are checked more strictly as E.164, and a quote sent without payment_method is priced as prepaid (business_fee_share 100), so only a prepaid create can bind it.Listo sends your customer the payment link and the courier's messages
On a
card order, Listo now sends the customer the payment link itself. The
moment the order is created and waiting for payment, it goes out as a WhatsApp
from Listo’s own number, in Spanish, with the amount owed and payment.url, and
if that WhatsApp does not reach the customer Listo sends the same link by SMS.
payment.url is still on the response, so you can show it or keep it, but you no
longer need to send it, and you should not: the customer would get the same link
twice from two senders. Nothing in the schema changed, and no partner had created
a card order before this, so no existing integration changes behaviour.We are also documenting something that was already true and that the docs
denied. Listo was already sending the customer of every API order its courier
messages, from the same number: courier assigned, on the way, arriving and
delivered, with the tracking link and the drop-off code when you asked for one.
That includes prepaid and cash orders, and the line saying Listo never meets
your customer on a prepaid order was wrong. See
what Listo sends your customer.a plan awaiting its first payment, and a spent plan refuses deliveries
A store whose first Plan Listo month your balance and card could not cover now reads
plan.status awaiting_payment instead of past_due, and Listo charges it by itself once they can. A plan whose included deliveries for the cycle are all used refuses deliveries with 402 plan_required.each restaurant can pay its own Plan Listo
Your account can be set up so that each restaurant pays its own Plan Listo, and then a live store is created with
plan null and cannot be api_only.a quote fixes what the order is charged, and booking differences land on your ledger
GET /v1/ledger has two new row types: requote_adjustment, explained below, and listo_charge_correction, a charge Listo made in error and gave back.A quote used to read as a promise that you would pay exactly what you were
quoted, however the underlying cost moved before the delivery was created. That
promise is withdrawn. Binding a quote on POST /v1/deliveries still charges the
order the quote’s fee_cents and split, and the total your customer sees never
moves. What changes is what happens when the courier network books the trip at a
slightly different price: the difference is no longer absorbed for you. It
appears on your ledger as its own requote_adjustment row on GET /v1/ledger,
negative when the booked trip cost more and positive when it cost less.This should be rare: a create now books the very quote you were shown whenever it
is still valid, and asks for a new one only when it has expired or the order
changed what it was priced for. A cash order always books a fresh quote, because
the quote you were shown carries no amount for the courier to collect.
requote_adjustment is a new type value, and type values grow in ordinary
releases, so keep counting the ones you do not recognise toward your balance.balances show the cash still in review
GET /v1/balance and GET /v1/stores/{store_id}/balance carry in_review_cents, cash still under review that deliveries cannot draw on.a busy courier network answers 409 no_couriers_available
When every courier near a trip is busy, the courier network refuses to price it
for the moment. That refusal used to reach you as
503 provider_unavailable,
which says our dispatch is down, and your monitoring counted it as a server
error. It now answers 409 no_couriers_available with retryable: true, on
POST /v1/quotes and on POST /v1/deliveries, and nothing is created or
debited. The identical request, with the same Idempotency-Key on a create,
goes through once a courier is free, usually within minutes.no_couriers_available is the word a delivery that ended without a courier
already carries in failure_reason, so one fact now has one name wherever you
meet it. If you retry every 503 at once, this refusal no longer reaches that
loop: retry it after a few minutes, a bounded number of times, and then tell
whoever is waiting for the order. provider_unavailable still means what it
says, a dispatch outage.the service area is 15 km, and its edge refuses with out_of_service_area
The distance Listo delivers from a store is now 15 km in a straight line from
the store’s position, up from 10 km. A quote or a delivery whose drop-off is
farther than that is refused with
422 out_of_service_area, as before, at the
new distance.A drop-off within a few tens of metres of that edge can also be refused by the
courier network, which measures the same 15 km from its own record of the
store. That refusal used to reach you as 503 provider_unavailable with
retryable: true, which invited you to retry a distance no retry can change.
It now answers 422 out_of_service_area with retryable: false, on
POST /v1/quotes, on POST /v1/deliveries, and on
POST /v1/deliveries/{id}/retries, where the delivery stays at no_courier
and nothing is charged. A delivery the network refuses this way after it was
created reads failure_reason: delivery_failed, as any other failure of ours
does.Rarely, the refusal comes on POST /v1/deliveries itself, a moment after the
network priced the trip. The create answers the same 422, with the same body,
and replaying it with the same Idempotency-Key answers it again. The delivery
had already been recorded by then, so it stays, ended at no_courier under your
external_id, with its fee debit returned in full and nothing else charged. To
send that order somewhere else, create it with a new external_id.Repeating a create with the same Idempotency-Key while the original request is
still being processed, its dispatch included, answers 409 idempotency_conflict
(in progress) until the original has answered, and from then on the replay
returns the original’s answer, that 422 included. A repeat that arrived while
the dispatch was still running used to be answered 201 from the delivery,
even when the original went on to answer 422.If you already branch on out_of_service_area, there is nothing to change. If
you retry every 503, a drop-off at the edge now stops at the first answer
instead of at your retry limit.the drop-off arrival time is null once a drop-off fails
dropoff_eta_at is when the courier is expected at the customer. After a
failed drop-off it used to carry a different time: from the moment a delivery
entered returning it held when the courier was expected back at the store,
kept moving as the courier drove there, and stayed on the delivery once it was
returned. It is now null on a returning and on a returned delivery, in
every response and every webhook that carries a delivery, because the customer
is not receiving anything and there is no arrival at their door left to
describe. The time the order gets back to the store is not published in its
place.If you show dropoff_eta_at to your customers or your merchants as an arrival
time, a returning order now shows none, where before it showed when the food
would be back in the kitchen as though it were arriving at the customer.
returned still marks the moment the order is back.Nothing about the request or the response shape moved: the field is still
always present, and null was already one of its values before the courier had
an estimate.every live store carries a Plan Listo, charged to you
Every store you create with a live key now carries one Plan Listo, and the plan
is charged to you: MX$1,990 plus IVA a month for each store at list price, or the
price you agreed with Listo.
POST /v1/stores with a live key charges the new
store’s first month in the same request, from your live balance or from the card
saved on your account, so fund one of them before you create a store and send an
Idempotency-Key on every live create. A month paid from the balance appears on
GET /v1/ledger as a plan_charge row, and every ledger row now carries
store_id, the store it belongs to, or null when it belongs to none.A live store whose plan is unpaid or has ended has its quotes, deliveries and
retries refused with 402 plan_required, whatever your balance holds. Every
store object carries a new plan block, which is null on a test store and on
a live store created before Plan Listo existed: those keep requesting couriers
as before. Three new endpoints manage a plan:
POST /v1/stores/{id}/plan_cancellation ends it when the period already paid
runs out, DELETE on the same path undoes that, and POST /v1/stores/{id}/plan
restarts one that ended. A restaurant that connects an account of its own and
already pays its own plan keeps paying it until its next renewal, and from then
on it is charged to you. The store object in store.updated and
store.connected now documents plan, access_tier, billing and
markup_cents as always present; the last three were already sent on every
event, and the reference now says so. Every plan block also carries courtesy,
always present: it is true while Listo waives the store’s plan, when nothing is
charged and next_charge_on is null.Everything about it is in plan and billing.deliveries by car, and a cart too large for one trip is refused
A quote and a delivery take
vehicle (moto or car), and manifest.items[].size (small, medium, large or xlarge) decides when a trip needs a car; a cart too large for one trip is refused with 422 order_too_large.a connected restaurant's stores carry the business name
A store’s
name is the name you gave it, and for every store you created that
is unchanged. A restaurant that connects an account with several branches now
arrives with each store named after the business and the branch, Tortería La Mera Mera, Zapopan, the way its own customers read it, where it used to arrive
as the bare branch (Zapopan). A branch whose own name already begins with the
business’s name, such as Tortería La Mera Mera Centro, is published as it is.
The same name comes back from the exchange, on store.connected and
store.updated, and on GET /v1/stores. When a connected restaurant renames
its business, you receive a store.updated for each of its stores, carrying the
new name.Renaming a store with PATCH /v1/stores/{id} now renames its business with it
when the two carry one name, which every store you created does. Before, only
the store’s own name moved, so what the restaurant’s customers were shown still
led with the old one. A branch of a restaurant with several branches, or a store
whose name already differs from its business’s, is renamed alone and reads as
the business followed by the new name. Sending back the name you read is safe: a
name that begins with the business’s name and a comma is read as the business
and the branch, so it renames nothing, and Tortería La Mera Mera, Centro
renames only the branch.A store.updated is no longer sent when the store object it would carry is
exactly the one you were last sent for that store, so a save that changes
nothing you can see sends nothing, and one change is one event. The event for a
store whose card readiness changed is sent as before, although its object does
not change.Nothing about the request or the response shape moved.same-day withdrawals, and a refusal while a pickup point is confirmed
Withdrawals can be same-day:
POST /v1/withdrawals takes kind, GET /v1/balance carries same_day, and a withdrawal carries kind, fee_cents and due_at. A store whose pickup point is still being confirmed is refused with a retryable 409 pickup_point_pending.a store's timezone follows its position, and deliveries can originate on WhatsApp
A delivery’s
origin has a third value, whatsapp: a delivery created from an order a customer placed on WhatsApp and the store accepted.POST /v1/stores and PATCH /v1/stores/{id} accept a timezone, and a zone
you sent used to be kept exactly as sent. It is now the fallback: the store’s
own position decides its zone, and what you send is used only when the position
cannot answer (a store with no coordinates, or a position no zone claims).Sending a zone that disagrees with the position changes nothing and is not an
error. The store is created or updated, everything else you sent is applied,
and the response carries the zone the position gives, so what you read back is
always what the store actually keeps.The timezone change moves nothing in the request or the response shape, and
the field is not deprecated. What changed is which of two answers wins. We reversed it because
we cannot tell the two cases apart from here: a zone sent by default out of a
form that never asked its user arrives looking exactly like a zone somebody
chose, and a store on the wrong clock silently moves its opening hours, its
delivery timestamps and the week its earnings settle in. If a store’s clock
looks wrong to you, its coordinates are what to fix.stores can split the delivery fee by a fixed amount
A store’s
default_fee_split_mode and default_fee_split_amount_cents tell you when the restaurant splits the delivery fee by a fixed peso amount instead of a percentage, so read the mode before default_business_fee_share.a wasted trip is billed at the delivery fee
A
provider_charge on GET /v1/ledger, and the matching entry in a delivery’s
charges array, used to be what the wasted trip cost Listo. It is now the
delivery fee for each leg that was actually driven, less your own store markup:
one leg for a cancellation late enough that a courier was already working on the
order, two for a return trip after a failed drop-off, and none at all when no leg
was driven.Nothing about the response shape moved. The same fields and the same types come back, and no
integration has to change. What changes is the amount: expect larger values on the same orders from this date, and if you bill
these charges on to your own merchants, bill what the row says rather than a
figure you worked out before.editable drop-off notes on the way, and stores outside Mexico are refused
PATCH /v1/deliveries/{id} now also takes dropoff.notes, so you can correct what the courier is told about the drop-off while the order is on its way, and it answers 409 not_editable once it is too late. A store outside Mexico is refused with 422 unsupported_country. The PATCH reference also stops listing four request fields the endpoint never read.a delivery whose drop-off failed is now returning
A delivery whose drop-off failed now has the status
returning while the courier brings the order back to the store, until it ends in returned. Its charges are already complete on the first returning event.refund your customer's card payment
You can refund your customer’s card payment with
POST /v1/deliveries/{delivery_id}/refunds, and every delivery carries refunds, refunded_cents and refundable_cents.withdrawals carry a tracking key and need a registered bank account
A withdrawal now carries
clave_de_rastreo, the SPEI tracking key your bank uses to find the deposit, once it is paid, and POST /v1/withdrawals is refused with 422 payout_account_not_ready until you have a bank account registered.connect a restaurant that already has a Listo account
POST /v1/connect_requests and POST /v1/connections connect a restaurant that already has a Listo account, with its owner’s approval, and the new store.connected webhook tells you when it happens. POST /v1/stores also accepts owner_email.your own store ids, idempotent store creation and a delivery origin
A store carries your own
external_id, which you can send on create and update and filter GET /v1/stores by, and POST /v1/stores honours Idempotency-Key, answering 409 for a key reused with another body. Each delivery also says where it came from in origin, which GET /v1/deliveries can filter by.deliveries accept a tip for the courier
POST /v1/deliveries accepts tip_cents, a tip for the courier that every delivery carries back. The reference now marks charges, access_tier, billing and markup_cents as always present, which they already were.a cap on your share of the delivery fee
POST /v1/quotes and POST /v1/deliveries accept business_fee_cap_cents, a ceiling in cents on your share of the delivery fee.stores have an access tier and a markup, with their own balance and ledger
Stores now have an
access_tier (api_only, wallet or full) and a markup_cents, and billing says whose balance pays their deliveries. A store’s own balance and ledger are at GET /v1/stores/{store_id}/balance and GET /v1/stores/{store_id}/ledger, and POST /v1/stores/{store_id}/owner invites the person who runs it to its Listo console.deliveries carry their charges, and the ledger lists your balance movements
Each delivery now carries
charges, what a cancelled or returned trip cost you, and GET /v1/ledger lists every movement on your balance. The reference also shows the shape of a retry that fails.the first published version
The first published version of the Partner Delivery API.