Payouts

Bulk payouts

Pay hundreds or thousands of recipients in one batch — by uploading a CSV in the dashboard or by sending items through the API.

CSV format#

The first row must be a header. Column order doesn't matter; names are case-insensitive.

ColumnRequiredNotes
recipientYesSolana address. Aliases: address, wallet.
amountYesDecimal MUNY amount greater than zero, e.g. 250 or 12.5.
referenceRecommendedYour reference. Must be unique within the file.
emailNoRecipient email, for your records and notifications.
nameNoRecipient display name.
payouts.csv
recipient,amount,reference,name
7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU,100,AFF-001,Ahmed
9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM,250,AFF-002,Sarah

Validation#

Every row is validated before anything is created. Nothing is sent until you confirm the review screen.

CodeWhen
invalid_addressThe recipient is not a valid Solana address.
missing_recipientThe recipient cell is empty.
missing_amountThe amount cell is empty.
zero_amountThe amount is zero.
invalid_amountNot a plain decimal number (no signs, exponents or currency symbols).
too_many_decimalsMore decimal places than MUNY supports.
duplicate_referenceThe same reference appears on more than one row.
duplicate_rowAn exact duplicate row without a reference.
malformed_csvUnterminated quotes, wrong column count, or an unreadable file.
missing_columnThe recipient or amount column is missing.

Paying the same address more than once in a batch is allowed, but is flagged as a warning (repeated_recipient) so you can double-check it. The review shows the number of valid recipients and the total MUNY required.

Validate a CSV over the API

ts
const report = await muny.payoutBatches.validateCsv(csvText);

report.total_recipients; // 2478
report.total_amount;     // "612940"
report.errors;           // [{ row: 14, field: "recipient", code: "invalid_address", message: "…" }]

Create a batch#

const batch = await muny.payoutBatches.create({
  name: "October affiliates",
  items: [
    { recipient: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", amount: "100", reference: "AFF-001", name: "Ahmed" },
    { recipient: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", amount: "250", reference: "AFF-002", name: "Sarah" },
  ],
  idempotency_key: "affiliates-2026-10",
});

The API re-validates every item and rejects the whole batch with 422 if any row is invalid, returning each problem in error.details. A batch either is created completely or not at all.

Batch lifecycle#

Each row becomes its own payout with its own lifecycle. The batch status summarises them: queued → processing → completed, or completed_with_errors if any payout failed. Batches can also start in awaiting_approval.

GET /v1/payout-batches/:id
{
  "id": "b2f1…",
  "object": "payout_batch",
  "name": "October affiliates",
  "status": "processing",
  "total_amount": "612940",
  "confirmed_amount": "473120",
  "total_network_fees_sol": "0.009655",
  "counts": { "total": 2478, "queued": 412, "processing": 132, "confirmed": 1931, "failed": 3, "cancelled": 0, "draft": 0, "awaiting_approval": 0 }
}

When every payout reaches a final state you receive payout_batch.completed. Individual payouts also emit their own payout events.

Retries and export#

  • Failed items stay retryable. POST /v1/payout-batches/:id/retry-failed re-queues only the failed payouts — confirmed payouts are never sent again.
  • GET /v1/payout-batches/:id/export returns a results CSV with each row's status, signature and failure reason.
  • List a batch's payouts with GET /v1/payout-batches/:id/payouts?status=failed.
Batches are limited to 10,000 rows. Split larger runs into several batches.

In the dashboard#

  1. Upload

    Drop a CSV on Payouts → Bulk payout.
  2. Fix errors

    Every invalid row is listed with its line number and reason.
  3. Review

    Check recipients, total MUNY required and the funding wallet.
  4. Confirm

    The batch is created and processed in the background with live progress.