A batch is not one transaction — it is many independent orders that happen to start together. Each recipient gets their own quote, their own deposit address and their own status, so a payout list can cross chains and assets freely, and one bad line never holds up the rest.
- Recipients can be paid in different assets and networks in the same run.
- Failures are per recipient: below-minimum amounts are refused at creation, before money moves.
- Idempotency keys make the whole run safe to re-execute after a crash.
- There is no on-chain multisend contract involved, and no pooled balance anywhere.
From a CSV to a finished run
-
Validate the list before you price it
Every row is
address, asset, network, amount. Run each one throughGET /v1/validate-address— a keyless call — and reject the file rather than the payout.known:falsemeans the network has no pattern to check against, which is "unverified", not "valid". -
Quote every row
One quote per recipient, by the amount they should receive (
amount_to). The sum of the returnedFromAmountvalues is what the run will cost you — approve it before anything is created. -
Create the orders, a few at a time
Five to ten concurrent creations is the right pace: providers throttle bursts, and each order must be created and funded inside the offer's five-minute life. Every creation carries an
Idempotency-Keybuilt from the run id and the row. -
Fund each order
Each one has its own
DepositAddressand exactAmountFrom. This is where the network fees live: a hundred recipients is a hundred outgoing transfers from your wallet. -
Collect the results
Poll each order to a terminal status and write the run's report:
DONEwithTxID, or a reason and a row to retry.
The fan-out
import pLimit from "p-limit";
const limit = pLimit(8); // providers dislike bursts
const results = await Promise.all(rows.map((row, i) => limit(async () => {
try {
const order = await createPayout(runID, row, i); // quote → POST /orders
return { row: i, ok: true, id: order.ID, send: order.AmountFrom,
to: order.DepositAddress };
} catch (e) {
return { row: i, ok: false, error: String(e) }; // one row, not the run
}
})));
const payable = results.filter((r) => r.ok);
const rejected = results.filter((r) => !r.ok); // show these before funding
The idempotency key is what makes the run restartable. Derive it from the run and the row, never from the clock:
const key = `${runID}:${row.address}:${row.amount}`; // stable across retries
// POST /v1/partner/orders with header "Idempotency-Key: "
// A repeat returns the same order — a crashed run resumes instead of duplicating.
When a row fails
| What you see | Why | What to do |
|---|---|---|
400 amount below minimum | The row is under that provider's MinFromAmount for the pair. | Check MinFromAmount at quote time; batch small amounts into fewer, larger payouts, or use a different asset. |
400 offer expired | More than five minutes passed between quoting and creating. | Re-quote that row. Keep quote → create → fund tight. |
400 provider is disabled | The exchange went offline after the quote. | Re-quote; the next offer comes from another provider. |
| No offers at all | No provider serves that pair right now. | Pay that recipient in an asset that /v1/assets lists. |
| TIME_EXPIRED after funding was skipped | The deposit never arrived in the provider's window. | Re-quote and re-create; nothing was sent, so nothing is lost. |
| REFUNDED | The swap failed after funding. | The provider returns the coins to refund_address — always set it to your own wallet. |
How this differs from an on-chain multisend
| Multisend contract | SwapZilla batch | |
|---|---|---|
| Chains | One, the contract's own | Any pair the providers cover, mixed freely in a run |
| Assets | The one you already hold | Each recipient's own — converted on the way |
| Transactions | One, cheap per recipient | One deposit per recipient — the cost of not pooling funds |
| Custody | None | None |
| Failure | Usually all-or-nothing | Per recipient, with a reason |
Not included, and worth planning around: there is no single batch endpoint, no one funding transaction, no webhooks, and no fiat leg. A run is a loop your side drives — the API makes each iteration safe.
What people fan out
- Affiliate and referral payouts — hundreds of small amounts, each to whichever asset the partner asked for.
- Creator and bounty programmes, where the payout list changes every cycle and the addresses are user-supplied.
- Airdrops and rewards that must land in the recipient's preferred network rather than yours.
- Refund and compensation runs, reconciled row by row with a
TxIDagainst each one.
Questions
Is there a single endpoint that takes a list of recipients?
No. A batch is created as one order per recipient — the API has no bulk endpoint. In practice this is what makes mixed-asset, mixed-network runs possible at all, and it means each recipient carries their own status and their own failure reason.
Can I send to a hundred addresses on different chains in one run?
Yes. Each order names its own destination asset and network, so a run can settle TRC20 USDT, BTC and ETH side by side from the same source asset. What is shared is only your run id, which you set as client_order_id on each order.
What stops a crashed run from paying someone twice on restart?
A stable Idempotency-Key per row — derived from the run id and the recipient, not from the time. Repeating a creation with the same key returns the order that already exists instead of opening a second one.
How many orders can I create at once?
SwapZilla applies no rate limit, but providers throttle bursts of quoting, so eight to ten concurrent rows is a comfortable pace. The real constraint is the five-minute offer expiry: each row has to be quoted, created and funded inside it.
Who pays the network fees?
You pay the fee on each deposit you send, and the provider's payout fee is already reflected in the offer's ToAmount — which is why quoting by amount_to is the reliable way to make a recipient's figure exact.
Can I see the whole run afterwards?
GET /v1/partner/orders?page=1&limit=100 lists your orders newest first, paged, and each carries the client_order_id you set — so the run reassembles from your own prefix. Individual orders can also be refreshed synchronously with POST /v1/partner/orders/{id}/refresh.
What else the same key builds
- Crypto payment links and invoicesPay-by-link and invoicing for freelancers and contractors — quote by the amount due, settle straight to the payee's wallet.
- Crypto payment processing and acquiringCheckout for shops and SaaS: accept 100+ assets, settle in one, reconcile by your own order id.
- Crypto exchange API, SDK and swap widgetEmbed swaps in a wallet, bot, aggregator or website — streaming quotes, one order call, your own UI.
- Crypto payroll for contractors and remote teamsRecurring payouts to a contractor roster: each person's own asset and network, exact net amounts, per-run reconciliation.
- Your own MCP server for AI agentsThe published swapzilla-mcp server in one line — or your own, on your partner key, with allow-lists and spending caps.
- Streaming quotes, live rates and price alertsServer-sent quote streaming, an always-fresh rate feed and order tracking you can turn into alerts.
Getting a key
Register in the partner cabinet and your partner id and the api_key that goes in X-API-Key are issued at once. The key starts working once it is activated: write to @swapzilla_support_bot with a sentence about what you are building. Every endpoint, field and status is in the partner API reference.