Products

Vendor settlements: pay suppliers in crypto and reconcile by webhook

A supplier sends an invoice; weeks later someone wires it, then someone else opens the bank export, finds the matching line, and ticks the invoice closed. Crypto removes the wire and the export but not the reconciliation — unless the payment tells your system when it has settled. halfin vendor settlements pay suppliers as single or mass payouts and emit a `payout.completed` or `payout.failed` webhook per payment, so the invoice that triggered the payment is the thing that gets marked paid, automatically, against a verified event rather than a hand-matched statement.

01

The cost center isn't the payment — it's the reconciliation

Vendor settlement is rarely a volume problem the way affiliate or payroll runs are. You might pay a few dozen suppliers a month, on their terms, against their invoices. The expensive part is the back office: matching each outbound payment to the right invoice, chasing the ones that didn't go through, and keeping accounts payable in agreement with what actually left the treasury. Bank rails make that worse, because the only signal you get back is a statement line you have to interpret after the fact.

Paying in crypto by hand swaps one reconciliation chore for another. You copy an address out of a supplier email, send a transfer, then go update the invoice in your AP system from memory or a block explorer. When a transfer times out ambiguously — the transaction may already be on-chain even though your client never saw a response — a re-send risks paying the same invoice twice, and now your AP ledger and your treasury disagree until someone notices.

halfin closes that gap from both ends. Each settlement carries a caller-supplied idempotency key, so re-sending after a timeout returns the payout halfin already created instead of paying an invoice twice. And once a payment settles, a signed webhook tells your system which payout completed — so the invoice it was raised against gets marked paid by an event you can verify, not by a human reading a statement.

02

One settlement per invoice, keyed so it can't double-pay

Model a settlement as a payout whose identity comes from the invoice it pays. Every payout names a currency, an amount as a string, a destination address, and a deterministic idempotency key — and for vendor settlement the natural key is the supplier's invoice number, optionally joined with the supplier id. Because that key is reproducible, a retry after a crash or a timeout reproduces the same key, and halfin matches it to the payout it already holds. One invoice maps to exactly one payment, however many times your worker re-runs.

Settlements rarely move in lockstep, so treat them as single payouts when invoices arrive one at a time and as a mass payout when you batch a settlement run — a weekly or month-end sweep of everything approved. There is no separate batch state to learn: a run is a fan-out over the single-payout API, and the idempotency keys make the whole loop safe to interrupt. A bad line — a malformed address, an amount below a chain's dust threshold, an asset the supplier can't receive — is rejected on its own and reported back; you fix that invoice and re-submit, and the settlements that already succeeded are no-ops.

Nothing leaves the treasury the moment your integration calls the API. Each payout enters a pending-approval state and is released from the dashboard, so a staged settlement is a proposal a human signs off. For accounts payable that approval step is where the controls live: your system can assemble a settlement run unattended from approved invoices, and a finance operator confirms the total and the supplier list before any funds move.

03

What a settlement line carries

Each supplier payment is described by the same small set of fields. Amounts are always strings, never floating-point numbers, so an invoice total is transported exactly with no binary-rounding drift between your AP ledger and the chain. Currency and network are chosen per line, so you pay each supplier on the rail they invoiced in — a manufacturer in USDT on Tron, a SaaS vendor in USDC on Base, a freight partner in native SOL — within the same run.

FieldTypeNotes
currencystringAsset + network, e.g. USDT (TRC-20 / ERC-20 / Solana), USDC (ERC-20 / Solana / Base), BTC, ETH, SOL.
amountstringInvoice total as a decimal string. Never a JS number — the settled amount stays exact end to end.
destinationstringSupplier's address; validated for the chosen network before the line is accepted.
idempotency_keystringDeterministic per invoice, e.g. supplier id joined with the invoice number. Re-submitting returns the original payout.
04

Settle from code, mark invoices paid from the webhook

A settlement run walks your approved-invoices list and submits each one with a key built from the invoice number. The only headers a request carries are `X-API-Key` and `Content-Type`; the idempotency key is a snake_case field in the request body, not a header. Because each call is idempotent, wrapping the loop in a retry is correct rather than dangerous — the second pass converges on exactly one payment per invoice — and after the run is staged, a finance operator releases it from the dashboard before anything settles.

The reconciliation happens on the way back. halfin emits a `payout.completed` webhook when a settlement has actually settled to the depth its chain requires, and a `payout.failed` webhook when a line needs attention. Verify the HMAC signature on every event before you act on it, then look up the payout by the idempotency key you minted — your invoice number — and flip that invoice to paid or back to needs-review without anyone reading a bank statement.

# Stage a settlement run; each line is keyed by its invoice, so re-running is safe.
while IFS=, read -r supplier invoice 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\": \"settle-$supplier-$invoice\"
    }"
done < approved-invoices.csv
# An operator releases the staged run in the dashboard before funds move.
# Then a payout.completed webhook (HMAC-signed) lets you mark each invoice paid.
05

Closing the loop without polling

Without webhooks, keeping AP in sync means polling each payout until it settles — wasteful, slow, and easy to get wrong at the tail of a run. The two settlement events remove the polling entirely: subscribe once, and halfin pushes the outcome of every supplier payment to your endpoint. The key you chose at submission time is what lets the event find its invoice, which is why the idempotency key being the invoice number pays off twice — once for safety, once for reconciliation.

Settlement is reorg-aware and uses per-chain confirmation thresholds, so a `payout.completed` event means the supplier's funds have genuinely settled, not merely been broadcast. That distinction is what makes the webhook safe to act on: when you mark an invoice paid, it is paid to the depth that chain requires. A `payout.failed` event — a rejected address, an underfunded balance, a chain-level failure — routes the invoice back to your review queue instead of silently stranding it.

  • Single payouts for invoices that arrive one at a time; a mass payout for a month-end settlement sweep — same guarantees, same keys.
  • Per-line network choice — settle each supplier on the rail they invoiced in (USDT/Tron, USDC/Base or Ethereum, native SOL, BTC).
  • Operator approval before any funds leave the treasury — the staged run is a proposal AP signs off, not an instant transfer.
  • Idempotency key = invoice number — re-running a run after a crash pays each invoice exactly once.
  • `payout.completed` / `payout.failed` webhooks (HMAC-signed) mark invoices paid or for-review without polling or statement-matching.
06

Funding a settlement in the asset the supplier invoiced

A supplier paid in USDT on Tron is settled from a USDT-on-Tron balance; one invoicing in USDC on Base draws on USDC on Base. When your treasury holds one asset but a settlement run needs another, balance conversion is the bridge — convert asset-to-asset before you stage the run so every line has funds behind it. Balance conversion is crypto-to-crypto treasury movement, not a fiat off-ramp, and it doesn't pay a supplier on its own; it just makes the right asset available on the right chain when the run is approved.

The same primitives back a single payout when one supplier needs settling off-cycle — an early payment, a correction, a final invoice — and the mass-payout flow when a whole settlement run goes out together. Vendor settlement is simply the supplier-and-invoice shape of the same guarantees: stage from approved invoices, fund each line in the asset it was invoiced in, approve once, and let the `payout.completed` / `payout.failed` webhooks reconcile the run back into accounts payable.