Products

Automated payouts: drive mass payouts on your own schedule or trigger

A mass payout run still has a person at the centre of it: someone exports the recipient list, opens a terminal, and runs the file by hand on payout day. Automated payouts move that trigger off the person and onto your own system. A scheduled job or an event in your application fans out over POST /api/v1/payouts, each line carrying its own idempotency_key, and the staged payouts wait in pending-approval for a human to release them. The run becomes a thing your infrastructure does on a cron or a webhook — not a calendar reminder somebody has to remember.

01

The problem: payout day depends on a person being at a keyboard

Mass payouts already solve the hard correctness problem — submit the whole recipient set as one batch, let idempotency keys absorb every retry, and reconcile against a clean per-recipient result. But the trigger is still manual. Someone has to remember it's Friday, pull the latest list from your ledger, and run the file. When that person is on holiday, the run slips. When the list is built by hand from a stale export, recipients are missed or double-counted before halfin ever sees the request.

The other half of the problem is timing. Some payouts shouldn't wait for a weekly batch at all — a withdrawal a customer requested an hour ago, a commission that vests the moment a referral converts, a settlement that should fire when an order is marked complete. Holding those in a manual queue until the next scheduled run turns a real-time event into a once-a-week event, and the recipient feels the lag.

Automated payouts close both gaps by letting your own system own the trigger. The recipient set comes straight from your database at the moment it runs, so it's never stale. The run fires on a schedule you control or on an event in your application, so a customer-initiated withdrawal and a weekly affiliate sweep use the same primitive at different cadences. And because the call is still the ordinary single-payout endpoint, automating it adds no new surface to learn — it's the loop you'd run by hand, moved behind a scheduler.

02

Schedule-driven and trigger-driven are the same fan-out

There is no separate automation endpoint and no "batch" object to manage. An automated payout run is a fan-out over POST /api/v1/payouts: your worker walks a recipient set and submits each line, exactly as a hand-run loop would. The only thing that changes is who starts the loop — a cron entry, a queue consumer, or a handler reacting to an event in your application.

Schedule-driven runs cover the recurring cadence: an affiliate sweep every Friday, a creator earnings cycle at month end, a contractor payroll run on the 1st. The job reads the eligible recipients from your own records at run time, derives a key per line from those records, and submits the set. Because the key is deterministic, a job that restarts mid-run — or fires twice because two cron nodes both woke up — converges on exactly one payout per recipient instead of paying anyone twice.

Trigger-driven runs cover the event cadence: a customer requests a withdrawal, an order is settled, a referral converts. The handler builds a one- or few-line payout from the event and submits it immediately, with a key derived from the event's own identity — the withdrawal id, the order id — so a retried webhook or a re-delivered queue message can't create a second payout. Same endpoint, same idempotency guarantee, just a different thing pulling the trigger.

03

The idempotency_key is what makes automation safe

Automation multiplies the number of ways a run can fire more than once: a cron that overlaps with its previous tick, a queue that re-delivers on a consumer crash, a deploy that restarts a worker mid-loop, two replicas that both pick up the same schedule. Every one of those is a retry in disguise, and a naive automated loop would turn each into a duplicate payment. The idempotency_key is the single field that defuses all of them.

Send idempotency_key as a snake_case field in the JSON request body — it is not an Idempotency-Key header. Derive it deterministically from your own ledger so the same recipient in the same run always reproduces the same key: an affiliate's account id joined with the payout period, or a withdrawal's own id for an event-driven run. When halfin sees a key it has already accepted, it returns the original payout rather than creating a new one, so the entire automated run is safe to re-trigger from the top.

TriggerCadenceKey derived from
Scheduled cron jobRecurring (weekly, monthly, period close)Recipient id + payout period, e.g. account + 2026-06.
Queue / worker consumerWhenever the job is dequeuedRecipient id + run id, stable across re-delivery.
Application eventReal time (withdrawal, settlement, vesting)The event's own id, e.g. the withdrawal or order id.
04

Staging the run does not move the funds

Automating the trigger does not remove the human control point — it makes it more important. Each payout your job submits enters a pending-approval state and is released from the dashboard before any funds leave your balance. So a scheduled run, or a burst of event-driven payouts, produces a set of proposals, not irreversible transfers. Your infrastructure can stage thousands of payouts unattended overnight, and a treasurer still signs off in the morning before anything settles.

That separation is exactly what makes unattended automation tolerable. A bug in your scheduler, a bad export, or a duplicated event can at worst stage extra payouts — it cannot drain the balance on its own, because nothing is released without the approval step. The high-throughput programmatic path and the deliberate human gate stay distinct: the machine proposes the run, a person confirms it, and the idempotency keys guarantee that confirming a re-run never pays anyone twice.

05

Driving a run from a scheduled job

The example below is the body of a cron job. It reads the eligible recipients from your own records, then fans out over the single-payout endpoint, one idempotent call per line. Request headers are only Content-Type and X-API-Key; the idempotency_key rides in the body. If the job is killed and the scheduler retries it, the second pass re-submits the same keys — already-created payouts come back unchanged, and only the missing ones are created.

Nothing here is automation-specific on halfin's side. The same loop you'd run by hand becomes an automated run purely by putting a scheduler in front of it, because the safety lives in the idempotency_key rather than in any orchestration halfin provides.

#!/usr/bin/env bash
# Cron entry: 0 9 * * 5  -> Friday 09:00 affiliate sweep.
# Reads recipients eligible for this period straight from your own ledger.
PERIOD="2026-06"

your-ledger export-eligible-payouts --period "$PERIOD" \
| while IFS=, read -r account currency amount destination; do
    curl -sS -X POST https://api.thehalfin.com/api/v1/payouts \
      -H "Content-Type: application/json" \
      -H "X-API-Key: $HALFIN_API_KEY" \
      -d "{
        \"currency\": \"$currency\",
        \"amount\": \"$amount\",
        \"destination\": \"$destination\",
        \"idempotency_key\": \"sweep-$PERIOD-$account\"
      }"
  done
# Scheduler retried the job? Keys collide -> nobody is paid twice.
# Every staged payout waits in pending-approval until released from the dashboard.
06

What automation does and does not change

Automated payouts are the mass-payout guarantees with the trigger moved off a person — they keep every property the manual run has, and add nothing you have to reason about separately. The amount stays a string end to end, each line still picks its own currency and network so a single run can mix USDT on Tron, USDC on Ethereum or Base, and native SOL, and settlement stays reorg-aware with per-chain confirmation thresholds before a payout reports complete.

What automation does not do is bypass the approval gate or invent a recurring-charge mechanism. halfin does not pull from a saved instrument on a schedule; an automated run proposes payouts that a human still releases. If you need to pay the same recipients every cycle, you schedule a job that submits a fresh idempotent run each period — the cadence lives in your scheduler, and the control point lives in the dashboard.

  • Trigger lives in your system: cron, queue, or an application event — not a halfin scheduler.
  • Same endpoint as manual mass payouts: a fan-out over POST /api/v1/payouts.
  • Per-line idempotency_key (body field, snake_case) makes overlapping ticks and re-deliveries safe.
  • Every staged payout still waits in pending-approval and is released from the dashboard.
  • Per-line currency and network choice; reorg-aware settlement with per-chain confirmation thresholds.