Batch USDT payouts, up to 500 at a time
Paying one person is a request. Paying five hundred is a pipeline, and a pipeline is judged on what it does when something goes wrong: what to ask before ordering, what a partial outcome means for the invoice, and what makes a retry safe rather than expensive.
Estimate first, always
One order carries up to 500 transfers, and the estimate takes the same 500. It reserves nothing and returns what an order would compute: per recipient, the kind it would be priced as, the energy_units behind that, and any warnings.
curl -s https://api.nrg.market/v1/estimate \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"transfers":[{"to":"TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9"}]}'
# up to 500 of them in one call
That makes it the right place to build most of the integration: batching, address handling, prices and nearly all of the response parsing, repeated as often as the rate limits allow and costing nothing. It prices the run at the tariff in force now — the pricing page shows what that is.
What it cannot rehearse is the order. POST /v1/orders spends real TRX in every mode, and mode A cannot be tried free even once: the energy is delivered whether or not you send. Further out of reach are partially_completed and the causes behind a failed position — no request you can send produces them, so you meet them first in production and should review that handling as code that has never run. One piece of the flow does have a genuine free rehearsal: POST /v1/webhooks/{id}/test puts a test event through the real delivery path, same secret and signature, and reports what your receiver did with it.
Read the warnings per recipient
Three warnings can come back against an item, meaning three different things to a payout run (the failures behind them). blacklisted is a recipient frozen in the USDT contract: an order naming one is refused, and the estimate still lists it, so you see what will drop out of the batch before the order says so. inactive_recipient is not a problem to fix — it explains a price: this address has never held USDT, so the transfer needs roughly 131,000 units of energy instead of roughly 65,000 (why that is). contract_recipient says the address is a contract, not a wallet: accepted, priced from a dry-run, and worth a look against your records.
Which is the argument for estimating first: a frozen address does not announce itself in a list of hundreds, and screening ahead is free where the attempt costs energy.
What partially_completed means for the invoice
The order is one call carrying the same list, answered 202 with a Location to poll and a Retry-After saying how often. A batch does not have to succeed or fail as a unit, and the accounting says so. completed is energy delivered on every position, charged in full. partially_completed is the mixed outcome: some positions were not delivered through our fault, so the order is charged for what was delivered and released for the rest. The halves always add up to the original reservation — a discrepancy is a defect on our side, caught by a daily reconciliation, not by you.
failed means nothing was delivered through our fault, reservation back whole; canceled is your own cancellation before procurement started. Each failed position carries a cause from a closed set, so a mixed batch is attributed line by line, not argued about in aggregate.
Webhooks instead of a polling loop
At batch size, polling every order is wasteful and slow. Register a receiver and subscribe to what a payout run needs — order.ready, order.completed, order.partially_completed, order.failed, plus the per-position order.item.sent and order.item.expired. The body is {event, event_id, sent_at, data}, signed with an HMAC over the timestamp and the raw body; verify against the bytes you received, not JSON you re-encoded. Check the timestamp against a window of a few minutes: a signature with no freshness check is replayable forever.
Deduplicate on event_id — it is the same across every attempt at one event, including a redelivery you ask for by hand. Answer 2xx quickly and work afterwards: a slow answer counts as a failed attempt, and enough failed deliveries in a day take the receiver out of service. That is a one-way door. Nothing switches a disabled receiver back on — not the API, not the cabinet, not us — so the cure is registering a new one, which issues a new secret. GET /v1/webhooks is the only place that state shows, and it never heals itself.
The idempotency key is the safety rail
Every order takes an Idempotency-Key, bound to the raw bytes of the body. The same key with a byte-identical body returns the same order and the same 202; with a different body it is a conflict, not a second order; a request rejected by validation does not consume the key. Practically: serialise the body once and repeat those exact bytes — another key order in your JSON, or one extra space, is another body.
The result is an unremarkable pipeline. Derive a key from your batch identifier, send, and retry the same bytes on any uncertainty: a network timeout on five hundred transfers becomes a question with an answer rather than a gamble on paying twice.