← halfin journalMay 11, 2026 · 8 min read
Product

Mass payouts vs single payouts: the same endpoint, a different mindset

Both go through POST /api/v1/payouts and both land in pending-approval — so the choice isn't an API decision, it's about who owns the loop and the ledger.

LT
L. TanakaProduct
product · cover

There is no batch endpoint. A mass payout is a single payout you wrote a for loop around. Once you internalize that, the product decision gets a lot simpler.

People come to us expecting two different APIs: a small one for "send this person money" and a big one for "send four thousand people money." They open the reference looking for POST /payouts/batches, don't find it, and assume they're reading the wrong page.

They're not. Single payouts and mass payouts ride the exact same call: POST /api/v1/payouts, one request per recipient. A mass payout is a fan-out — your runner loops a list and fires the call once per line. The platform doesn't hold a batch object you can query later. The batch lives in your code and your reconciliation. So the real question isn't "which endpoint" — it's "who owns the loop, and how much can break before someone notices."

What's identical, and why that matters

Before the differences, the things that don't change between sending one payout and sending ten thousand:

  • The request shape. Every line is currency (a crypto asset, e.g. USDT_TRC20), amount, destination, and an idempotency_key — the same body the API reference at docs.thehalfin.com documents. There's no network field — the asset is the rail. The how-do-I-create-a-payout walkthrough is the same whether you call it once or in a loop.
  • The approval gate. Every payout enters pending-approval and waits. Funds don't move until someone releases it from the dashboard. One payout, one approval click. Ten thousand payouts, still a release step before money leaves the balance. This is the safety property that makes a fan-out survivable: a runaway loop produces a pile of pending payouts, not a pile of sent ones.
  • The webhooks. You get payout.completed or payout.failed per payout, never per "batch." Whether a payout was line 1 of 1 or line 3,704 of 4,000, it reports its own outcome on its own event. Verify the HMAC over the raw bytes before you act on either.

So the API doesn't know whether you're running a single payout or a mass one. That's the point. The distinction is operational, not technical.

When a single payout is the right reach

Reach for the single-payout path when the send is discrete, attended, and rare enough to look at:

  • A supplier invoice you're settling by hand this week.
  • A one-off refund-by-payout to a customer who paid you on-chain.
  • A treasury move to a known address you've used before.
  • Anything where a human is going to eyeball the destination and the amount before clicking approve.

The tell is that the approval is the work. You're not racing a clock or a spreadsheet — you're making a deliberate decision about a single transfer. You don't need a runner, a results file, or a retry strategy. You need one correct call and one careful approval.

If you'd be comfortable doing it by hand in the dashboard and only automating to save typing, it's a single payout. The automation is a convenience, not a system.

When it's actually a mass payout

It's a mass payout the moment the list is long enough that you can't watch it, and re-runnable enough that you'll re-run it:

  • An affiliate or CPA cycle paying hundreds or thousands of accounts.
  • Creator or streamer disbursements on a weekly cadence.
  • Trader or player withdrawals batched into a run.
  • Marketplace seller settlements at end of period.

Here the approval gate is no longer the work — the loop is the work. And a loop that touches money has exactly one hard requirement: it must be safe to re-run. Your process will die at line 1,200. Your machine will reboot. The network will time out on a response that actually succeeded server-side. If re-running the file double-pays the first 1,199 people, you don't have a payout system, you have a liability.

This is what the per-line idempotency_key is for. It is not optional decoration on a mass run — it is the load-bearing field. Derive it deterministically from the business action (the payout-cycle id plus the recipient line, say), send it as a snake_case field in the request body — it is not an Idempotency-Key header — and a replayed line returns the original payout instead of minting a second one. We wrote a whole post on building a payout runner that survives a crash mid-file; the short version is that the idempotency key is the difference between "re-run the file" and "manually diff two thousand rows at 2am."

The decision in one table

Single payoutMass payout
EndpointPOST /api/v1/payoutsPOST /api/v1/payouts, in a loop
Who owns the batchNobody — it's one sendYour runner + ledger
ApprovalOne release clickReleases for the whole run
Idempotency keyNice to haveMandatory — it's your crash guard
Failure blast radiusOne transferThe whole run, if the loop isn't idempotent
The actual workApproving correctlyWriting a re-runnable loop

The columns share an endpoint and an approval model. They diverge on one axis: who is responsible for the batch. For a single payout, nobody is — there is no batch. For a mass payout, you are, because the batch only exists inside your runner.

The trap

The mistake we see is treating a mass payout like a single payout that just happens to run more times. Teams write the loop, skip the idempotency key because "it worked in the test of three rows," and ship it. It works for weeks. Then a deploy restarts the process halfway through a Friday run, the cron retries the whole file, and the affiliates who were already paid get paid again. Now you're clawing back funds from people who did nothing wrong, which is a support and trust problem, not just an accounting one.

The inverse mistake is rarer but real: building a heavyweight runner — queue, dead-letter table, the works — for a list of four suppliers you settle once a month. That's a single payout you over-engineered. Approve it in the dashboard and move on.

Operating rule

Don't choose by volume — choose by ownership. If you'd be comfortable approving it by hand and the automation only saves keystrokes, it's a single payout: call the endpoint, release it, done. The moment the list is long enough that a human can't supervise it and the run is something you'll re-run, you've crossed into mass-payout territory, and the price of admission is a deterministic idempotency_key on every line. Same endpoint, same approval gate, completely different mindset.

L. Tanaka, halfin product

↳ end of articlehalfin journal · May 11, 2026