Batch stablecoin payouts are a run of many individual payouts, such as 200 contractors, paid from one funded stablecoin balance, each converted to local currency at its own live quote. There is no batch price and no batch endpoint on Lumx. Every row gets its own quote, idempotency key and status, and the run ends when every row is final.
Paying 200 people exactly once depends on quote timing, limit checks, safe retries and how the run is closed. This guide covers each for a payer that holds USDC and pays in BRL and MXN.
Why one quote cannot price a whole batch
A quote is a price for one conversion. It fixes a rate for a window of time, and the transaction that uses it consumes it, so a batch of 200 recipients needs 200 quotes and 200 payouts. A quote that fixes the local amount means a contractor owed BRL 4,000.00 gets BRL 4,000.00.
The payer's cost is therefore the sum of the rows, known only when the last one is quoted. That is why the funding section carries a buffer.
On Lumx, a locked quote is created with POST /exchange-rates, with type set to LOCKED, a rail such as PIX or SPEI (Mexico's interbank transfer system, run by Banxico), and either a source amount in USDC or a target amount in local currency. The id in the response goes into the payout request as exchangeRateId, and a second use fails with EXCHANGE_RATE_ALREADY_USED. Every row is its own quote call and its own payout call, so a batch on Lumx is a loop your system runs, and each row is its own transaction with its own status.
How should a batch run be structured?
Quote one row, pay it, move to the next. Quoting every row first works only for a handful of rows, because most quotes expire. A locked rate lasts 30 seconds by default, with 1 minute and 5 minutes available, and only the 30-second lock is free. So quote and pay each row in turn: the gap between creating a quote and spending it should be seconds. If finance needs a sign-off total first, use a floating rate for the estimate, never for the run. What a longer window is worth is in how long a payment quote lasts.
On Lumx there is no batch endpoint: the run sends one quote call and one payout call per row, and rows can run in parallel from your job runner, each worker quoting and paying its own row inside the window, with balance and limit headroom checked for the whole set. Find the pace the API tolerates in the sandbox, handle 429 TOO_MANY_REQUESTS with exponential backoff and keep the run resumable. The run uses the default 30-second lock, so no lock fee applies. A row that waits past its window fails with EXCHANGE_RATE_EXPIRED, and the fix is a new quote.
Check the run against the limits before the first row
A run that crosses a limit halfway leaves part of the file paid and the rest in the support queue: recipients told payday is today, a ticket per unpaid row, two half-files to reconcile by hand. For a business with standard KYB (know your business, the verification of a company and its owners), the documented defaults are 50,000 dollars per transaction, 100,000 per day and 100,000 per month, counted per transaction type.
Does the file fit a standard KYB customer? Hypothetical 400-dollar average, published limits
Rows | Total | Fits the daily limit | Fits the monthly limit |
200 | 80,000 | Yes | Yes, with 20,000 left |
300 | 120,000 | No | No |
For standard KYB the daily and monthly ceilings are the same figure, so splitting the file across days does not help. The remedy is a limit increase, with a supporting document and a published review target of one business day. The tiers are in limits and cut-offs by rail.
On Lumx, reading the customer with includeTransactionLimits=true returns the per-transaction maximum and the used and remaining amounts for the daily and monthly windows. A payout that would pass a limit is refused with a 422 and TRANSACTION_LIMIT_EXCEEDED. Check the destinations in the same pre-flight pass: a destination goes through verification before it can be used, and destinations.approved is the webhook that says a payee is ready. A destination that ends in destinations.final_rejection is a row removed from the run before it starts, not a failure found halfway. More reasons are in why payouts get rejected.
How much stablecoin should the wallet hold before the run?
Enough for the file, plus a buffer for drift. With fixed local amounts, a currency that weakens during the run makes later rows cost more stablecoin. The buffer is arithmetic, not a forecast: if every row of an 80,000 USDC run moved against the payer by an assumed 0.5%, the run would need 400 more. How fees add to the total is in how payout pricing works.
On Lumx, every customer has a wallet on each supported blockchain that keeps its balance between operations, so the payer holds the file's USDC in its own Lumx wallet and pays every row from it. That is not pre-funding in the Lumx sense: no capital sits in settlement accounts before a payment, and conversion and settlement happen when each payout starts. What it costs to hold that balance ahead of a run is in float and cost before payouts. A payout larger than the balance fails with INSUFFICIENT_BALANCE, so the pre-flight check compares the quoted total against the balance as well as the limits.
Retries that cannot pay a row twice
A timeout is the dangerous moment: the request may have succeeded and the client cannot tell. The defense is an idempotency key per row, saved before the call.
Lumx takes an Idempotency-Key header on every POST, PUT and PATCH request, and the value has to be a UUID v4. Generate a UUID per row on the first attempt, store it against the run and the row, and reuse it on every retry of that row. Keys expire after 24 hours.
What each documented outcome means for a row, and what the run should do
Situation | Documented result | What the run does |
Same key, same body | Cached response, with the header | Treat as the original answer. No second payout |
Same key, different body | 409, | A bug in the run: the body changed. Stop that row and inspect |
Same key, original still processing | 409, | Wait and read the transaction. Do not send under a new key |
Quote past its window | 422, | Request a new quote, then pay under a new key |
Quote already consumed | 422, | An earlier attempt may have paid. Check your record of the row before quoting again |
Balance too low | 422, | Pause the run, fund the wallet, resume |
Limit reached | 422, | Wait for the window, lower the amount or request a higher limit |
Provider timeout or network failure | 503, | Marked safe to retry, but failed responses are cached per key. Check your run record and the transactions list for the row, then retry under a new key |
I would never quote the whole file before a run starts, whatever its size, because a quote is a short-lived promise and a file is a long-lived intention. I treat a batch as 200 independent payments that happen to share a wallet, not as one payment with 200 lines, and I would start any new run with ten rows from the real file in the sandbox before the full one goes out.
A failed response is cached too, so retrying a failed payout under the same key returns the same error. Responses to POST, PUT and PATCH are cached per key for 24 hours, so reusing a key on a quote call returns the cached answer rather than a fresh quote. New quote, new key. After a 5xx there is no transaction id to look up, so the run reads the transactions list for the customer, paginated with size and cursor, and matches each transaction's metadata against the row reference it sent. The match happens on your side. Confirm in the sandbox that your run id and row number come back in metadata, and keep the run's own record of every row to compare against the list.
On Lumx, match errors on the code field, never on the message, which is subject to change, and keep the requestId for support.
Track the run to the end with webhooks
The loop ending tells you every request was sent, not that every recipient was paid. Close the run on final statuses, row by row.
A Lumx payout moves through TRANSFERRING_STABLECOIN, TRADING and TRANSFERRING_FIAT to SUCCESS, or ends in FAILED. Each step has a webhook event: offramp.transferring_stablecoin, offramp.trading, offramp.transferring_fiat, offramp.success and offramp.failed. Every event carries a unique eventId, the field to deduplicate on, and the payout id that ties it to a row.
A row-level status table for the run
Row status | Means | Run action |
Processing | The payout exists and is moving | Wait for the events |
Succeeded |
| Mark the row paid |
Failed |
| Fix the cause, then add the row to a new run under a new key |
A succeeded payout means the funds left the banking partner. A transaction held for an RFI (request for information, a compliance hold that asks for a document) can sit in processing longer than its rail suggests.
The operator map shows who else works on this flow. On Lumx, the payout request accepts a metadata object, so put the run id and row number there. GET /transactions/{id} confirms any single payout, and webhook endpoints are set up under Developers, then Webhooks, in the dashboard.
Running the same batch every month
A recurring run is the same run started by a clock the payer owns: a cron job or a workflow tool calling the same loop.
Store the rows (destination, amount, purpose, reference), the state of the last run so a restart resumes instead of repeating, and the key for each row of the current run. Never store a quote: it lives 30 seconds, or 5 minutes at most, and a run that reuses last month's exchangeRateId fails on the first row with EXCHANGE_RATE_EXPIRED.
When each rail processes a payout, for the rails in this guide, from the published cut-off table
Currency | Rail | Cut-off, local time | Settlement |
BRL | PIX | None | Instant, 24/7/365 |
MXN | SPEI | None | Instant, 24/7/365 |
GBP | FPS | None | Instant, up to 1 million pounds per payment, 24/7/365 |
EUR | SEPA | None. 100,000 euros or more between 4:30 PM and 2:30 AM UK time routes to SEPA Credit Transfer | Instant; Credit Transfer same or next business day |
USD | ACH | 2:00 PM ET | 1 to 2 business days |
USD | FEDWIRE and SWIFT | 3:00 PM ET | Same business day; SWIFT 1 to 5 business days |
Pix (Brazil's instant payment system, run by the Central Bank) and SPEI have no cut-off and no weekend, so a Brazilian or Mexican payroll can fire at any hour. See paying contractors across Latin America and paying contractors in Brazil.
On Lumx, the monthly run is your scheduler calling the same quote and payout loop, and a payout submitted after a cut-off, on a weekend or on a bank holiday is processed on the next business day of the rail's jurisdiction, per the cut-off page.
When a batch run is the wrong tool
A quote per row gives each recipient a firm local amount, not the payer a firm total, and a fixed total over weeks is a forward contract from a bank or broker. A payer who pays other businesses' people needs each seller onboarded as its own customer. The platform case is in seller payouts for a marketplace.
How a 200-row run goes with Lumx
Take a hypothetical file of 200 contractors, 120 paid in BRL over PIX and 80 in MXN over SPEI, averaging 400 dollars. Each recipient is a destination under the paying customer, with the holder relationship stated truthfully: EMPLOYEE for contractors, SUPPLIER for vendors.
Budget for the run, hypothetical file
Item | Arithmetic | Result |
API calls | 200 rows, one quote and one payout each | 400 calls |
Sequential run time | 200 pairs at an assumed 2 seconds each, against a 30-second window per row | About 7 minutes, no row near its window |
Funds needed | 200 rows at 400 dollars | 80,000 USDC against the 100,000 daily limit |
Drift buffer | 0.5% assumed against the payer | 400 USDC more |
The window is per row, not per run. Before row one, read the customer with includeTransactionLimits=true, compare the balance with 80,400, and confirm every destination has fired destinations.approved.
Then the loop: a locked quote per row with the local amount as the target, then the payout with the exchangeRateId, a purpose from the documented list, a fresh UUID as the key and the run reference in metadata. Failures become the next run.
Lumx is stablecoin payments infrastructure for businesses that move money between Latin America and the rest of the world: one API to collect, hold, convert, and pay out in BRL, MXN, COP, USD, EUR, and GBP or in USDC and USDT, over local rails such as PIX, SPEI, PSE, Bre-B, ACH, FEDWIRE, SEPA, and Faster Payments, with SWIFT and on-behalf-of payments and collections (POBO and COBO) in USD, EUR, and GBP, plus named virtual accounts, custodial wallets, and KYB/KYC built in.
For this run, the payer's wallet funds the file, Pix and SPEI settle without a cut-off, and one webhook endpoint reports every row. See the USDC to BRL and USDC to MXN pairs, the coverage page and global payments. The conversion is in what a stablecoin off-ramp is. Next step: open the sandbox, run a few rows from your real file, then force an expired quote and replay a key, following the idempotency page and the exchange rates page.
Verified on November 1, 2026. Operational context, not legal, tax, or investment advice.
Cover photo: Oxana Melis on Unsplash.
What are batch stablecoin payouts?
Batch stablecoin payouts are many individual payouts run together from one funded stablecoin balance, each converted to local currency at its own live quote. The batch has no shared price, so each row needs its own quote, idempotency key and final status. On Lumx there is no batch or bulk endpoint: the payer's system runs one quote call and one payout call per row, in parallel from its job runner if needed, and each row is its own transaction.
Can one quote cover several payouts in a batch?
No. A quote fixes one rate for one conversion and is consumed by the transaction that uses it, so a batch of 200 recipients needs 200 quotes. On Lumx the error is EXCHANGE_RATE_ALREADY_USED, and a quote created with POST /exchange-rates lasts 30 seconds by default, with 1 minute and 5 minutes available for a fee.
What happens to a quote that expires in the middle of a batch?
The payout that uses it is refused, and the row needs a new quote and a new payout request. On Lumx the error is EXCHANGE_RATE_EXPIRED with a 422 status, and because a failed response is cached under its idempotency key, the new attempt goes out under a new UUID after a fresh quote.
How do I avoid paying someone twice if a request times out?
Send an idempotency key on every payout, one per row, stored before the call, and reuse that key and body on retry. The retry returns the original result. After a timeout error, check your run record and the transactions list for the row before sending again, because a failed response stays cached under its key. On Lumx the key must be a UUID v4, it expires after 24 hours, and the same key with a different body returns a 409 IDEMPOTENCY_KEY_CONFLICT.
How do I know when every payout in a batch has finished?
Track each payout by its id through webhooks and close the run only when every row is final. On Lumx, offramp.success and offramp.failed are the final events, offramp.transferring_stablecoin, offramp.trading and offramp.transferring_fiat are the steps between, and success means the funds left the banking partner.





