Send crypto payouts from your own code
Payments are credited to your easyway balance, and easyway holds that balance until you withdraw it. This guide withdraws with the API instead of the dashboard: check the balance, create a payout that cannot be sent twice, follow it to sent and reconcile it later. Typical uses are a daily settlement to your own wallet or exchange account, or paying out to your own cold storage on a schedule.
Before you start
| What | Where |
|---|---|
An API key with payouts:write and payouts:read (plus balances:read to check the balance) | Dashboard → Developers. Use a separate key for payouts: a key that creates invoices does not need to move money. Add an IP allowlist if your server has a fixed IP. |
| Whitelisted destination addresses (live mode) | Dashboard → Payouts → Approved addresses, confirmed with a 2FA code (2FA must be on). One entry per coin and network; up to 50 per account. A TON comment or XRP destination tag is stored with the entry. |
| An account webhook with the payout events | Dashboard → Developers → Account webhooks, with payout.sent and payout.failed selected. A key's own webhook only receives invoice events. |
The whitelist is what makes API payouts safe: in live mode easyway refuses any address that is not on it, whatever the key says. The memo or tag also comes from the whitelist entry, never from the request, because on an exchange it decides whose account is credited.
1. Check what you can withdraw
// What can be withdrawn now, per coin and network
const res = await fetch("https://api.easyway.cash/v1/balances", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
},
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const page = await res.json();Each row has available (yours to withdraw now) and locked (reserved by payouts still in flight). Balances are per coin and network: USDT on TRON and USDT on Ethereum are separate rows, and a payout spends from the row of its asset.
2. Create the payout
Send the coin id, a whitelisted address and the amount as a decimal string. Always send an Idempotency-Key that names the action on your side, such as the settlement batch or the date: if the request times out you can repeat it with the same key and body and get the first payout back instead of paying twice.
Node.js (18+, built-in fetch)
// Withdraw 500 USDT on TRON to a whitelisted address
const res = await fetch("https://api.easyway.cash/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
"Idempotency-Key": "settle-2026-10-10",
"Content-Type": "application/json",
},
body: JSON.stringify({
"asset": "USDT_TRC20",
"address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"amount": "500",
"note": "Daily settlement"
}),
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();PHP (cURL)
// Withdraw 500 USDT on TRON to a whitelisted address
$ch = curl_init("https://api.easyway.cash/v1/payouts");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
"asset" => "USDT_TRC20",
"address" => "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"amount" => "500",
"note" => "Daily settlement",
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("EASYWAY_API_KEY"),
"Idempotency-Key: settle-2026-10-10",
"Content-Type: application/json",
],
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new Exception("easyway: " . $data["error"]);
}Python (requests)
# Withdraw 500 USDT on TRON to a whitelisted address
import os, requests
res = requests.post(
"https://api.easyway.cash/v1/payouts",
headers={
"Authorization": f"Bearer {os.environ['EASYWAY_API_KEY']}",
"Idempotency-Key": "settle-2026-10-10",
},
json={
"asset": "USDT_TRC20",
"address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"amount": "500",
"note": "Daily settlement",
},
)
res.raise_for_status()
data = res.json()The answer is 201 with the payout: an id (po_…), status pending and the network_fee. Save the id. The network fee is charged on top of the amount, in the coin itself: this example reserves 500 + 1 = 501 USDT. Every coin's fee and minimum is on the pricing page and in GET /v1/assets (payout_fee, min_payout).
| Error | Meaning |
|---|---|
400 invalid_address | Not a valid address for that network (checksums included). |
400 below_minimum_payout | Less than the coin's minimum (0.5 USDT on TRON). |
400 too_many_decimals | More decimals than the coin has on-chain. Amounts are never rounded for you. |
403 address_not_whitelisted | Live mode: the address is not on the whitelist for that coin and network, or not active yet. |
403 payouts_temporarily_locked | Payouts are frozen for the account for a while, for example right after 2FA was turned off. |
422 insufficient_funds | Amount + network fee is more than the available balance of that coin. |
422 idempotency_key_reused | The same Idempotency-Key was already used with a different body. Use a new key for a new payout. |
A failed request (any 4xx or 5xx) does not use up its Idempotency-Key, so you can fix the request and send it again with the same key.
3. Follow it to sent
| Status | Meaning |
|---|---|
pending | Accepted and reserved; waiting to be signed. It can still be cancelled in the dashboard. |
processing | Being signed and broadcast. It can no longer be cancelled. |
sent | Broadcast; tx_hash is the transaction on the chain's explorer. |
failed | Not sent; amount and network fee are back on your available balance. failure_reason says why. |
cancelled | You cancelled it while pending; the funds are back. No webhook is sent for this. |
Live payouts are not instant: the signing service picks them up in turn, and a payout stays pending until a wallet holding enough of that coin can pay it. Listen for the webhooks instead of polling in a tight loop. After the signature check:
// Runs after the signature check (see the webhook signatures guide).
// Payout events go to the ACCOUNT webhooks, never to an API key's own webhook.
async function handlePayoutEvent(event) {
if (await db.seenEvent(event.id)) return; // retries repeat the same event id
const p = event.data.payout;
if (!p) return;
const row = await db.withdrawalByPayoutId(p.id); // the id you saved from POST /v1/payouts
if (!row) return;
if (event.type === "payout.sent") await db.markWithdrawal(row.id, "sent", p.tx_hash);
if (event.type === "payout.failed") await db.markWithdrawal(row.id, "failed", p.failure_reason);
await db.rememberEvent(event.id);
}To check one payout by hand, call GET /v1/payouts/:id. The webhook signatures guide has the signature check for Express, Next.js, PHP and Flask.
4. Reconcile
GET /v1/payouts lists the key's mode newest first, filtered by status, asset and a range of UTC days with from / to. Page through until you have total rows and compare each id, amount and tx_hash with your own records:
const res = await fetch("https://api.easyway.cash/v1/payouts?page=1&pageSize=50&from=2026-09-01&to=2026-09-30", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
},
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();The dashboard also exports payouts as CSV for a date range, if your accountant prefers a spreadsheet.
5. Test it first
With a sk_test_ key the same calls work on your test balance: no whitelist is needed, the payout settles at once with a fake tx_hash (it starts with test_) and payout.sent is delivered to your test account webhooks. Fill the test balance by simulating invoice payments, as the sandbox guide shows. A test payout cannot show you a live payout waiting in pending, so make your code wait for the webhook rather than expect sent in the response.
Going live checklist
| Check | Why |
|---|---|
| A live key with only the payout scopes, behind an IP allowlist | Limits what a leaked key can do, on top of the whitelist. |
| Every destination whitelisted, with the memo or tag the exchange gave you | Without the right memo an exchange cannot tell whose deposit it is. |
| One Idempotency-Key per withdrawal you mean to make | A retry never pays twice; a new withdrawal never replays an old one. |
Your code waits for payout.sent / payout.failed | Live payouts take time; the create response is always pending. |
| Amounts as strings, at most the coin's decimals | Money never passes through floating point, and nothing is rounded silently. |
Every field and error is also in the payouts section of the docs and in /openapi.json. How balances are held is on the security page.