API

Bulk payouts

Pay many handles in one reviewed batch: weekly referral rewards, leaderboard prizes, contributor payroll. Every row is validated before anything moves, and invalid recipients are blocked — never skipped silently.

1 · Validate

POST/v1/payments/bulkscope payments:create

Validation resolves every handle, checks amounts and assets, flags duplicates and lookalikes, and totals the payable rows. Nothing is paid.

import { FomoPay } from "@fomopay/sdk";

const fp = new FomoPay({ apiKey: process.env.FOMOPAY_API_KEY });

// End of week: Top Recruiters
const batch = await fp.bulk.validate({
  name: "Referral rewards — week 41",
  vaultId: process.env.FOMOPAY_VAULT_ID,
  idempotencyKey: "REFERRALS_2026_W41",
  payments: [
    { recipient: "@lyra", amount: "500", asset: "USDG", note: "Top recruiter" },
    { recipient: "@mia", amount: "250", asset: "USDG", note: "Referral reward" },
    { recipient: "@frog", amount: "100", asset: "USDG", note: "Referral reward" },
    { recipient: "@builder", amount: "750", asset: "USDG", note: "Referral reward" },
  ],
});

console.log(`${batch.summary.valid} valid · ${batch.summary.invalid} unresolved · ${batch.totals.USDG} USDG`);
// 4 valid · 0 unresolved · 1600 USDG

if (batch.summary.invalid === 0) {
  await fp.bulk.execute(batch.batch_id);
}
Body
payments[]array ≤ 1,000required
recipient (@mia), amount (decimal string), asset, optional note.
vault_idstringoptional
The Reward Vault that pays. API batches execute from a vault; a batch with source: "wallet" is reviewed and paid by a person in the dashboard instead.
source"vault" | "wallet"optional
Implied vault when vault_id is set.
namestringoptional
Shown in the dashboard and on receipts.
Idempotency-Keyheaderoptional
Same key + same rows → the same batch, so a retried job never creates a second one.
201 Created — 4 valid · 1 unresolved
{
  "object": "bulk_batch",
  "batch_id": "batch_6h8k0m2p4r6t8v0x2z4b",
  "status": "VALIDATED",
  "source": "vault",
  "vault_id": "vault_3n5p7r9t1v3x5z7b9d1f",
  "name": "Referral rewards — week 41",
  "idempotency_key": "REFERRALS_2026_W41",
  "summary": { "total": 5, "valid": 4, "invalid": 1, "undecided": 1, "removed": 0, "deferred": 0, "payable": 4, "paid": 0, "failed": 0, "pending": 0 },
  "totals": { "USDG": "1600" },
  "paid_totals": {},
  "warnings": [],
  "review_url": "https://fomopay.family/dashboard/bulk/batch_6h8k0m2p4r6t8v0x2z4b",
  "items": [
    { "row": 1, "recipient": "@lyra", "amount": "500", "asset": "USDG", "note": "Top recruiter", "valid": true, "identity_state": "VERIFIED", "error_code": null, "decision": "INCLUDE", "payment_id": null, "payment_status": null, "tx": null },
    { "row": 2, "recipient": "@mia", "amount": "250", "asset": "USDG", "note": "Referral reward", "valid": true, "identity_state": "VERIFIED", "error_code": null, "decision": "INCLUDE", "payment_id": null, "payment_status": null, "tx": null },
    { "row": 3, "recipient": "@frog", "amount": "100", "asset": "USDG", "note": "Referral reward", "valid": true, "identity_state": "VERIFIED", "error_code": null, "decision": "INCLUDE", "payment_id": null, "payment_status": null, "tx": null },
    { "row": 4, "recipient": "@builder", "amount": "750", "asset": "USDG", "note": "Referral reward", "valid": true, "identity_state": "VERIFIED", "error_code": null, "decision": "INCLUDE", "payment_id": null, "payment_status": null, "tx": null },
    { "row": 5, "recipient": "@kestrel", "amount": "50", "asset": "USDG", "note": null, "valid": false, "identity_state": "NO_PAYOUT_WALLET", "error_code": "NO_PAYOUT_DESTINATION", "error_message": "This identity hasn't verified a payout wallet yet.", "decision": "INCLUDE", "payment_id": null, "payment_status": null, "tx": null }
  ],
  "created_at": "2026-10-12T19:30:00.000Z",
  "executed_at": null,
  "completed_at": null,
  "canceled_at": null
}

Each row carries its identity_state and, when it can't be paid, an error_code and a plain-language error_message: RECIPIENT_NOT_FOUND, NO_PAYOUT_DESTINATION, RECIPIENT_UNVERIFIED, UNSUPPORTED_NETWORK, UNSUPPORTED_ASSET, DUPLICATE_RECIPIENT … warnings groups rows that need a human look (lookalike handles, recent wallet changes). review_url opens the batch in the dashboard.

2 · Execute

POST/v1/payments/bulk/{batch_id}/executescope payments:create
// Row 5 (@kestrel) can't be paid. Say what to do with it — FomoPay never silently skips a row.
await fp.bulk.execute(batch.batch_id, { onInvalid: "defer" }); // or "remove"

// Follow progress
const latest = await fp.bulk.get(batch.batch_id);
latest.summary; // { paid: 4, pending: 0, failed: 0, deferred: 1, … }
Body
on_invalid"remove" | "defer"optional
Required when any row is invalid. remove drops those rows; defer keeps them on the batch to pay later once the recipient is ready.
confirmationsobjectoptional
similar_handle_acknowledged, recent_change_acknowledged, new_recipient_acknowledged — for rows the risk engine flagged.

Execution returns 202 and runs in the background: each payable row becomes a BULK payment from the vault and is tracked like any other payment — payment.* webhooks fire per row, with bulk_batch_id set. The batch ends COMPLETED, or PARTIAL if some rows failed or were deferred.

Invalid rows are never skipped silently

Executing a batch with undecided invalid rows is refused:

400 Bad Request
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "1 row can't be paid. Remove it or resolve later before paying (API: on_invalid \"remove\" or \"defer\").",
    "request_id": "req_…",
    "details": { "invalid_rows": [{ "row": 5, "recipient": "@kestrel", "error_code": "NO_PAYOUT_DESTINATION" }] }
  }
}

3 · Track

GET/v1/payments/bulk/{batch_id}scope payments:read

Returns the batch with per-row payment_id, payment_status and tx; paid_totals sums what has confirmed.

CSV

The dashboard (Bulk) imports the same rows from CSV — handle,amount,asset,note, header optional, any column order — validates them with the same rules, and lets a person remove or defer invalid rows before paying from a vault or their own wallet.

payouts.csv
handle,amount,asset,note
@lyra,500,USDG,Design bounty
@mia,250,USDG,Referral reward
@frog,100,USDG,Referral reward
@builder,750,USDG,Contributor payout