How to run batch payouts with live quotes: one quote per payout, expiry, and rate drift
How to pay 100+ recipients from USDC with live quotes: quote and execute per payout, size the funding, handle expiry and retries, and track the batch.
To run a batch of payouts with live quotes, treat each payout as its own quote-then-execute pair. A quote backs exactly one payout and expires after 5 minutes by default, so a batch of 120 recipients is 120 quotes, each executed right after it's created. Fund the wallet first, send an idempotency key per payout, and track each one to a terminal status.
The batch is a loop. What makes it production-grade is everything around the loop.
Key takeaways
- One
quote_id, one payout. A second payout against the same quote fails. - Don't quote the whole batch up front. Quotes expire in 5 minutes by default; pipeline quote and execute per row.
- Rows quoted later get later rates. Fund with a buffer for drift across the run.
- An
Idempotency-Keyper payout turns a timeout retry into a replay instead of a double payment. - A batch is finished when every payout id has a terminal status, not when the loop ends.
Why can't one quote cover a whole batch?
Because a quote is a price for one conversion to one bank account. It names one bank_account_id, one amount, and one fee calculation, and it backs one payout.
That is a feature. Every recipient in the batch gets a firm number: the receiver_amount on their quote is what lands. A single blended rate for 120 people would make each recipient's amount an estimate until the batch finished.
It also means the batch is only as good as its slowest row. If row 87 sits in a queue for six minutes, its quote expires, and you re-quote it. Nothing executes at a stale rate. The glossary defines quote window, re-quote, and binding quote if you need the vocabulary.
How should you structure a batch run?
Pipeline it: quote, authorize if needed, execute, next row. Three patterns show up in practice, and only one holds up at size.
| Pattern | How it works | Expiry risk | Rate exposure | When it fits |
|---|---|---|---|---|
| Quote and execute per row | Each row gets its own quote, executed within seconds | Low | Each row priced at its own moment | Any batch size. The default. |
| Approve on an estimate, then quote per row | Finance approves totals from one indicative quote, with a tolerance; the run quotes and executes each row | Low | Bounded by the approved tolerance | Batches that need sign-off before money moves |
| Quote everything, then execute everything | All quotes first, all payouts after | High once the batch passes a few minutes | Locked per row, but many will expire | Tiny batches only |
The second pattern is how most payroll teams run. Someone approves "about 97,800 USDC, plus or minus 1%" in the morning. The run quotes each row live. If the total drifts past the tolerance, the run pauses and asks again.
How much USDC should you fund before the run?
Enough for the total at current rates, plus a buffer for drift and fees. You won't know the exact total until the last row is quoted, because every row is priced at its own moment.
Illustrative example. A company pays 120 contractors in Mexico over SPEI, each a fixed $15,000.00 MXN, quoted on the receiver side. The numbers below are made up for the arithmetic, not BlindPay pricing.
- Batch total: 120 x 15,000 = $1,800,000.00 MXN.
- Effective rate on the first quote: 18.4075 MXN per USDC.
- USDC for the batch at that rate: 1,800,000 ÷ 18.4075 = 97,786.23 USDC.
- Suppose the peso strengthens 0.3% during a long run, so the effective rate falls to 18.3523. Each row then needs 15,000 ÷ 18.3523 = 817.34 USDC instead of 814.89.
- If every row priced at the later rate, the batch would need 1,800,000 ÷ 18.3523 = 98,080.47 USDC. That's 294.24 USDC more than the first estimate.
A 0.5% to 1% buffer covers a move like that. Anything left stays in your wallet. Short runs shrink the problem: the less time between the first and last quote, the less room for drift. For how big that float should be month to month, see no pre-funding in a stablecoin API.
How do you authorize funds for each payout?
It depends on where the USDC sits. The payout endpoint is the same either way; what changes is the step before it.
- External EVM wallet. Each quote returns a
contractpayload. You callapproveon the token contract for that quote's exact amount, then execute withquote_idandsender_wallet_address. That's one on-chain approval per row, so 120 rows means 120 approvals and their gas. The EVM payout guide has the full script. - Managed wallet. No client-side signing. You pass the wallet address as
sender_wallet_addressand execute. BlindPay-managed wallets are in beta and custodial, so this path trades the approval step for BlindPay holding the balance. - Solana and Stellar. Each has its own signing step per payout: a token delegation on Solana, an authorize-and-sign XDR on Stellar.
For large recurring batches, the per-row approval cost on EVM is worth pricing in. Who pays gas fees breaks it down by network.
How do you make retries safe?
Send an Idempotency-Key header on every payout call, one key per row, and resend the byte-identical body on retry. That's the whole defense against double payments when a request times out.
BlindPay's idempotency rules are specific:
| Retry situation | Response |
|---|---|
| Same key, same body, original finished | Original response replayed with Idempotency-Replayed: true. No second payout. |
| Same key, different body | 422 idempotency_key_payload_mismatch |
| Same key, original still running | 409 idempotency_key_in_flight with Retry-After |
Original returned 5xx | Key released, so the retry runs fresh |
Two practical rules. Derive the key from the batch and row, for example batch-2026-10-04-row-087, so a restarted job reuses it. And serialize the body once and keep the bytes: re-serializing JSON with a different key order counts as a different body. The IETF's Idempotency-Key header draft describes the same pattern across APIs, and the idempotency keys explainer covers it in more depth.
If a row's quote expired before the retry, the retry should request a new quote first. A new quote means a new payout body, so give it a new key.
How do you track a batch to the end?
By payout id, through webhooks. The loop ending tells you every request was sent, not that every peso landed.
BlindPay fires payout.new when a payout is created and its source funds are captured, payout.update on intermediate steps (including reaching failed), and payout.complete when a payout is confirmed at the destination or refunded. The webhook events reference lists them.
Keep a row-level status table and close the batch only when every row is terminal:
| Row status | Means | Action |
|---|---|---|
quoted | Quote created, not executed | Execute before expires_at or re-quote |
processing | Payout created | Wait for webhooks |
completed | Funds landed | Mark paid |
failed | Payout didn't complete | Fix the cause, requeue as a new row |
refunded | Stablecoins returned instead of paid out | Check the bank details, requeue |
On Pix and other non-USD local rails, a payout that can't be delivered after the stablecoins are collected is refunded automatically to the quote's refund wallet, or to the sending wallet when none is set. Recipients in Mexico can also check a SPEI transfer through Banco de México's CEP lookup, which helps when a contractor asks where their money is. More on the states in stablecoin payout statuses explained.
What should you test before the first real batch?
Run the whole loop on a development instance with test bank accounts and USDB, BlindPay's test stablecoin. Payouts there are free.
Test the unhappy paths on purpose: let one quote expire, retry one payout with the same idempotency key, and kill the job halfway and restart it. Development instances are capped at roughly 100 requests per minute, so pace a large test batch or you'll see 429 with a Retry-After header. The sandbox vs production guide lists what else differs.
Since SPEI runs around the clock per Banco de México, a Mexico batch doesn't have to wait for a business day. For a single exact-amount payout, start with paying an exact BRL, MXN, or EUR amount, and for marketplace-style runs see marketplace payouts in Latin America.
What to do next
Take last month's payout file, pick ten rows, and run them through a development instance with the quote-and-execute loop, one idempotency key per row. Then read the payouts reference for the status lifecycle before you scale it to the full batch.
When the same batch runs every month, how to schedule recurring payouts with live quotes covers what to store, when to fire per rail, and how to never pay twice.
FAQ
More payments articles
See all payments articlesHow live quotes handle volatile currencies: BRL, MXN, COP, and ARS
A live quote locks one payout's rate, not your budget. How BRL, MXN, COP, and ARS rates are set, and how to design payouts around moving currencies.
On/off ramp liquidity with live quotes: how it works and why it matters
What on/off ramp liquidity with live quotes means for developers: live quote vs batch rate, the quote ID flow in an API, what happens on expiry, how liquidity depth changes spread by transaction size, rate windows and UX, and a checklist for evaluating a live quote API.
7 real-world blockchain payment use cases: payroll, remittances, B2B, and more
Seven places blockchain payments beat bank rails: contractor payroll, remittances, B2B suppliers, marketplaces, 24/7 treasury, bill pay, and PSP payouts.