curl --request POST \
--url https://api.listo.delivery/v1/withdrawals \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"amount_cents": 250000,
"kind": "standard"
}
'import requests
url = "https://api.listo.delivery/v1/withdrawals"
payload = {
"amount_cents": 250000,
"kind": "standard"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({amount_cents: 250000, kind: 'standard'})
};
fetch('https://api.listo.delivery/v1/withdrawals', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.listo.delivery/v1/withdrawals",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'amount_cents' => 250000,
'kind' => 'standard'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.listo.delivery/v1/withdrawals"
payload := strings.NewReader("{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.listo.delivery/v1/withdrawals")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.listo.delivery/v1/withdrawals")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}"
response = http.request(request)
puts response.read_body{
"id": "wd_2xN6bK9mR4tQ7vC1sZ8pLwYe",
"amount_cents": 250000,
"currency": "MXN",
"status": "pending",
"requested_at": "2026-07-30T18:30:00Z",
"resolved_at": null,
"test": false,
"clave_de_rastreo": null,
"fee_cents": 0,
"kind": "standard",
"due_at": null
}{
"error": {
"code": "invalid_api_key",
"message": "The API key is missing, revoked, or not valid for this mode.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/invalid_api_key",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "insufficient_balance",
"message": "The account balance does not cover this request.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/insufficient_balance",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "read_only_key",
"message": "This API key is read-only. Use a key without the read-only setting to create, change or cancel anything.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/read_only_key",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "validation_failed",
"message": "Amount cents must be an integer",
"param": "amount_cents",
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/validation_failed",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}Request a withdrawal
Requests a transfer of amount_cents out of your balance to your
registered bank account. The money is reserved the moment the request is
accepted, so a second request cannot spend it twice.
The request is settled by a human, usually within a business day, and
the outcome arrives as a withdrawal.updated webhook rather than in
this response. Asking for more than available_cents is a 402.
You need a bank account registered before your first withdrawal, from
the dashboard rather than from this
API. Without one this call is 422 payout_account_not_ready and nothing
is reserved. The same refusal covers the day after you CHANGE the
account: a moved destination cannot receive money for 24 hours, and the
message names the instant it can again.
Same-day withdrawals
kind is optional and standard when you leave it out, which is the
free path above and is unchanged. Send "kind": "same_day" to have it
sent TODAY instead:
- It costs a flat fee, IVA included, which the response reports as
fee_cents. The fee is a SECOND debit and never a deduction, so your bank still receives the wholeamount_centsand your balance loses the two together. - It can draw on cash that is still on hold, which the free path cannot,
so
available_centsis not the bound here. - Requests are accepted around the clock. One made between 8 am and
8 pm Mexico City is sent within 3 hours; one made outside those hours
goes out the next morning.
due_aton the response is the deadline your request actually carries, stamped when you made it. - There is a minimum per request and a daily maximum per account.
Three refusals are specific to it and each names its bound in message:
402 insufficient_balance when the amount plus its fee is more than can
be withdrawn today, 422 same_day_cap_exceeded when the day’s maximum
has no room for it, and 422 same_day_below_minimum when the amount is
under the floor. A kind that is neither word is
422 validation_failed with param: "kind", never a quiet fallback to
the free path.
curl --request POST \
--url https://api.listo.delivery/v1/withdrawals \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"amount_cents": 250000,
"kind": "standard"
}
'import requests
url = "https://api.listo.delivery/v1/withdrawals"
payload = {
"amount_cents": 250000,
"kind": "standard"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({amount_cents: 250000, kind: 'standard'})
};
fetch('https://api.listo.delivery/v1/withdrawals', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.listo.delivery/v1/withdrawals",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'amount_cents' => 250000,
'kind' => 'standard'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.listo.delivery/v1/withdrawals"
payload := strings.NewReader("{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.listo.delivery/v1/withdrawals")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.listo.delivery/v1/withdrawals")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"amount_cents\": 250000,\n \"kind\": \"standard\"\n}"
response = http.request(request)
puts response.read_body{
"id": "wd_2xN6bK9mR4tQ7vC1sZ8pLwYe",
"amount_cents": 250000,
"currency": "MXN",
"status": "pending",
"requested_at": "2026-07-30T18:30:00Z",
"resolved_at": null,
"test": false,
"clave_de_rastreo": null,
"fee_cents": 0,
"kind": "standard",
"due_at": null
}{
"error": {
"code": "invalid_api_key",
"message": "The API key is missing, revoked, or not valid for this mode.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/invalid_api_key",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "insufficient_balance",
"message": "The account balance does not cover this request.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/insufficient_balance",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "read_only_key",
"message": "This API key is read-only. Use a key without the read-only setting to create, change or cancel anything.",
"param": null,
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/read_only_key",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}{
"error": {
"code": "validation_failed",
"message": "Amount cents must be an integer",
"param": "amount_cents",
"retryable": false,
"doc_url": "https://docs.listo.delivery/en/errors/validation_failed",
"request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10"
}
}Authorizations
Your API key, sent as Authorization: Bearer <key>. Live and test keys are separate; the key you send decides which world the call acts in. To try calls, use a test key (it starts with lst_test_): 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. A live key (it starts with lst_live_) books and charges real deliveries.
Body
Response
Created.
A request to move money out of your balance to your bank account.
The withdrawal id, wd_ prefixed.
"wd_2xN6bK9mR4tQ7vC1sZ8pLwYe"
What your bank receives. On a same_day withdrawal the fee is a
SECOND debit rather than a deduction, so this figure is what lands
either way and fee_cents is what your balance loses on top of it.
What the withdrawal cost, IVA included. Always 0 on a standard
withdrawal. Always present.
MXN standard is the free path: your released balance only, sent in 1
to 3 business days. same_day costs fee_cents, can reach cash
still on hold, and is sent the same day within the hours in
POST /v1/withdrawals.
standard, same_day pending until a human settles it. The move to paid or failed
is what withdrawal.updated announces.
pending, paid, failed When a same_day withdrawal is due to have been sent, stamped when
you requested it and never recalculated afterwards. Null on every
standard withdrawal: the free path promises a range of business
days rather than an instant.
Null while still pending.
The SPEI tracking key for the transfer: the identifier your own bank uses to locate the deposit. This is what you reconcile a payout against a bank statement with, and it is the same key you see in the dashboard for this withdrawal.
Always present, null until the transfer is sent. It stays null
forever on a failed withdrawal, because no transfer happened.
"MBAN01002608120001234567"