A CPA payout is many small, uneven amounts at once
The shape of a CPA payout is what makes it hard. A revenue-share program might pay a handful of large affiliates; a CPA network pays a long tail of publishers, each earning a per-action commission across multiple offers, and the per-line amount is different for almost everyone. The list is large, the values are uneven, and the cadence is fixed — weekly or per net-term — so the run repeats forever. None of that is a problem your tracker has; the tracker already computed every number. It's a problem the payout step has, because moving money to that many destinations by hand is where mistakes and delays live.
The first version of crypto CPA payouts is always a loop over an export: read a publisher row, send a transfer, move on. It holds up until the run is interrupted partway through — a dropped RPC connection, a wallet out of gas, a rate limit on row 400 of 1,200 — and now nobody can say which publishers were already paid. Re-running the file risks paying the first 400 again. Resuming by hand risks skipping someone in the middle. On payout day, under a publisher's expectation that they get paid on time, neither is acceptable.
The failure that quietly costs money is the double-pay. A send times out and the response is ambiguous: the transaction may already be on-chain even though your client never saw the confirmation. A naive retry treats that as a fresh payment and pushes a second one. On-chain transfers are final, so you discover it when treasury reconciliation comes up short or a publisher keeps the overpayment. halfin removes the ambiguity by requiring a caller-supplied idempotency key on every payout — re-submitting the same key returns the original payout instead of creating a new one, so the whole batch is safe to retry from the top.
What halfin owns and what your CPA platform keeps
halfin does not become your tracker, your anti-fraud engine, or your commission ledger. Those stay the systems of record for which conversions are valid after advertiser postbacks and scrubs, and what each publisher is owed for the period. halfin is the payment layer underneath: it takes the approved payout instruction your platform produces — one line per publisher, with the amount your ledger already calculated — executes it on-chain, and returns signed status your finance and partner-ops teams reconcile against. The decision about who gets paid, and how much, never leaves your platform.
That boundary is the point of the integration. Your network already runs offer management, postback validation, lead scrubbing, hold periods, and net-term accounting. halfin slots in at the end of that pipeline, after earnings are approved and a payout is authorized. The same primitives also cover the inbound side when the network collects advertiser deposits or budget top-ups in crypto, so the money flows in through invoicing or hosted checkout and out through payouts under one account.
- Your tracker stays the system of record for valid conversions, scrubs, holds, and amounts owed.
- Your anti-fraud, postback validation, and net-term logic stay entirely yours.
- halfin executes the approved per-publisher payout and returns signed settlement status.
- Optional inbound: collect advertiser deposits or budget top-ups via invoicing and hosted checkout.
One idempotent batch, one line per publisher
There is no separate batch endpoint to learn. A mass payout is a fan-out over the single-payout API: you submit one payout per publisher and reason about the set as a unit. Each line specifies a currency, an amount as a string, a destination wallet, and its own idempotency key. The key is the whole trick — derive it deterministically from your ledger, typically the publisher account id joined with the payout period, so the same publisher in the same cycle always maps to the same key. Because that derivation is reproducible, re-submitting the batch reproduces the keys exactly, and halfin matches each one to the payout it already created.
Amounts are always transported as strings, never floating-point numbers, so a per-action commission like "12.47" goes out exactly as your books recorded it — no binary-rounding drift between the ledger and the chain. That matters for CPA precisely because every line is a different small number; a system that quietly rounds a thousand uneven payouts will not reconcile.
Partial failure is the normal case, not an exception. Some lines get rejected up front — a malformed address a publisher pasted wrong, an amount below a chain's dust threshold, a currency the destination wallet can't receive — and those are reported per-line so the rest of the run still goes through. You fix the rejected rows and re-submit the whole file; the lines that already succeeded are no-ops, and only the corrected ones execute. No single bad publisher row blocks a thousand good ones.
| Field | Type | Notes |
|---|---|---|
| currency | string | Asset + network, e.g. USDT (TRC-20 / ERC-20 / Solana), USDC (ERC-20 / Solana / Base), BTC. |
| amount | string | Per-publisher commission as a decimal string — exact, no float rounding across the batch. |
| destination | string | Publisher wallet; validated for the chosen network before the payout is accepted. |
| idempotency_key | string | Deterministic per publisher + period, e.g. account id + cycle. Re-submitting returns the original payout. |
Staging the publisher run from code
The fastest integration maps your existing payout cycle straight onto the batch primitive. Take the approved-earnings export your tracker already produces, derive a deterministic idempotency key per publisher and period, and submit each line through the payouts API. Because each call is idempotent, wrapping the whole loop in a retry is correct rather than dangerous: a second pass after a crash converges on exactly one payout per publisher.
Payouts don't move funds the instant the API is called. Each one enters a pending-approval state and is released from the dashboard, so the batch your platform POSTs is a proposal, not an irreversible action. That keeps the high-throughput programmatic path — staging thousands of publisher lines unattended — separate from the human control point, where partner-ops or finance reviews the run and signs off before anything leaves the balance.
# Fan out a CPA payout run from the approved-earnings export.
# Each line is idempotent, so re-running after a crash pays no one twice.
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\": \"cpa-2026-06-$account\"
}"
done < approved_earnings.csv
# Re-run the same file after an interruption: keys collide, nobody is paid twice.Reconcile every payout against a signed webhook
Reconciliation is where a CPA payout run either closes cleanly or turns into a week of support tickets. The old process leaves a spreadsheet of "paid" checkboxes, which is not an audit trail, and a bank statement weeks later that doesn't tie a line back to a specific publisher and period. halfin's settlement leaves a concrete chain of evidence per payout: the idempotency key from your ledger, a dashboard record, an HMAC-signed webhook, and an on-chain transaction hash. When a publisher disputes a payment or finance closes the cycle, the trail is concrete rather than reconstructed.
Point a webhook endpoint at your platform so settlement state arrives independently of any API response you might have missed. Verify the HMAC signature before you mark a payout complete or move a publisher's balance — an unverified payload is not an event. The canonical payout event you act on is payout.completed; the invoice events (invoice.confirming, invoice.paid, invoice.underpaid, invoice.overpaid, invoice.expired) cover the inbound advertiser-deposit side if you collect funds in crypto. Match each payout.completed back to its idempotency key, write the transaction hash into your ledger, and you have a reconciliation loop that runs without a human comparing two spreadsheets.
- Verify the HMAC signature on every webhook before taking any business action — an unverified payload is not an event.
- Act on payout.completed to close out each publisher line; match it back to your ledger by idempotency key.
- Write the on-chain transaction hash into your records so support can answer "where's my payment".
- Use the invoice.* events for the inbound advertiser-deposit side if the network collects crypto.
- See the webhook event schemas and signature scheme at docs.thehalfin.com.
Pay each publisher on the chain they actually use
A global publisher base does not share one preferred rail, and forcing everyone onto a single network means rejected payments and addresses that can't receive the asset you sent. One publisher settles in USDT on Tron because the network fee is predictable at volume; a US-facing affiliate wants USDC on Ethereum or Base for their own accounting; another prefers Solana for fast, low-cost settlement on small per-action amounts. halfin lets each payout line pick its own currency and network, so one CPA run spans all of them without splitting the file by rail.
Settlement is reorg-aware and uses per-chain confirmation thresholds, so a payout reported complete has actually settled to the depth that chain requires — not merely been broadcast. That matters most at the tail of a large batch, where you want each publisher's status to mean "done", not "submitted and hopefully fine". Each line resolves to a transaction hash your support team can hand to a publisher who asks where their commission is.
| Publisher preference | Common rail | Why it fits CPA |
|---|---|---|
| Predictable fee on small, frequent payouts | USDT on Tron (TRC-20) | Widely held; low, predictable cost per line across a large publisher list. |
| US-facing accounting and reporting | USDC on Ethereum (ERC-20) or Base | Stablecoin many publishers reconcile against directly. |
| Fast settlement on many tiny commissions | USDT or USDC on Solana, or native SOL | Quick confirmation for high-frequency, small-value per-action payouts. |
| Native-asset payout | BTC, ETH, XRP, or an EVM L2 / BSC native token | For publishers who hold and want the base asset of a chain. |
Compliance stays the network's responsibility
halfin is deliberate about not overstating its role. It supports KYB onboarding when you become a merchant, applies AML awareness to the payment rail, and treats travel-rule considerations as a process to understand — none of which is a certification or license that halfin holds or grants. The network still owns publisher identity, the decision about which counterparties it will pay, wallet and sanctions screening before funds move, and the regulatory obligations of its own market. Keep affiliate vetting and the decision to pay inside your platform, hand halfin the approved instruction, and use the dashboard records and signed webhooks as the settlement evidence.
Start the integration with a small slice of the publisher list in the sandbox, confirm the reconciliation loop end to end — submit, release, receive payout.completed, verify the signature, write the transaction hash — then scale the same flow to the full base. The single-payout primitive backs both an on-demand payment to one trusted publisher and a full cycle fanned out over thousands; the per-payout guarantees don't change between them.