Skip to content
JuicePay

JuicePay documentation

Create account

Documentation

Send a payout batch

Dispatch thousands of legs in one request and handle partial failures.

A payout batch sends many transfers under one request, one approval, and one idempotency key. This guide covers constructing, submitting, and recovering a batch.

Build the batch#

Batches accept up to 25,000 legs. Each leg names its own recipient, amount, and optionally its own chain and asset.

ts
const batch = await juicepay.payouts.createBatch(
  {
    reference: `payroll-${run.period}`,
    funding: { asset: "USDC", source: "auto" },
    routing: {
      prefer_chains: ["base", "solana"],
      allow_bridging: true,
      max_network_fee: "1.25",
    },
    legs: run.recipients.map((recipient, row) => ({
      row,
      amount: recipient.net.toFixed(2),
      asset: recipient.preferredAsset ?? "USDC",
      recipient: recipient.walletId,
      reference: `payroll-${run.period}-${recipient.employeeId}`,
    })),
  },
  { idempotencyKey: `payroll:${run.period}` },
);

Two details carry most of the weight:

  • row is echoed back in every event, so you can map results to source rows without depending on array order.
  • Per-leg reference makes your own reconciliation trivial and gives support staff something human-readable to search.

Validate before you submit#

Dry runs validate every leg without moving value.

bash
curl -s -X POST "$JUICEPAY_URL/v1/payouts/batches?dry_run=true" \
  -H "Authorization: Bearer $JUICEPAY_KEY" \
  -H "Idempotency-Key: payroll-2026-08-19-run-2-dry" \
  -d @batch.json | jq '.legs[] | select(.valid == false)'

Validation catches malformed addresses, recipients failing sanctions screening, legs that would breach policy limits, and insufficient balance at the batch total.

Watch the batch progress#

A batch moves through created → authorising → executing → settled, or to partially_settled if some legs fail permanently.

bash
curl -s "$JUICEPAY_URL/v1/payouts/batches/bt_8kL3mQ" \
  -H "Authorization: Bearer $JUICEPAY_KEY" | jq '.status, .counts'
json
"partially_settled"
{
  "total": 10000,
  "settled": 9987,
  "failed": 3,
  "pending": 10
}

Legs retry automatically with backoff. A leg only reaches failed once the retries are exhausted or the error is classified as permanent — an invalid address, a blocked recipient, or an insufficient balance that you have not topped up.

Recover the stragglers#

Query the failures, fix the cause, and replay only those legs.

ts
const failed = await juicepay.payouts.listLegs(batch.id, { status: "failed" });

const repaired = failed.data.map((leg) => ({
  ...leg,
  recipient: correctedRecipientFor(leg.reference),
}));

await juicepay.payouts.replayLegs(batch.id, {
  legs: repaired.map((leg) => ({ row: leg.row, recipient: leg.recipient })),
});

Replaying a leg that already settled is a no-op, because the leg key is derived from the batch key and the row index. That is what makes it safe to replay a slightly stale failure list.

Failure classification#

Error codeClassWhat to do
recipient_invalid_addressPermanentFix the destination and replay the leg.
recipient_screening_blockedPermanentEscalate to compliance; do not replay.
insufficient_balanceTransientTop up and replay.
network_congestedTransientLeave it; retries handle it.
chain_haltedTransientRetries resume when the network resumes.
routing_unavailableTransientRelax prefer_chains or enable bridging.

Treating a permanent error as transient is how batches loop forever. Treating a transient error as permanent is how payroll ends up short. Classify deliberately.

Approval flow#

If your account requires approvals, the batch pauses at authorising. Approvers see the total, the recipient count, and any legs that tripped a warning. Approving commits the batch; rejecting returns it to created with your reason attached.

Approval windows are configurable, and an unapproved batch expires rather than sitting indefinitely — a stale payroll batch is a liability.

Reporting#

bash
curl -s "$JUICEPAY_URL/v1/payouts/batches/bt_8kL3mQ/report.csv" \
  -H "Authorization: Bearer $JUICEPAY_KEY" -o payroll.csv

The CSV includes one row per leg with the final status, chain, transaction hash, network fee, and your reference — enough to close the run without touching the API again.