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.
| Column | Required | Notes |
|---|---|---|
recipient | Yes | Solana address. Aliases: address, wallet. |
amount | Yes | Decimal MUNY amount greater than zero, e.g. 250 or 12.5. |
reference | Recommended | Your reference. Must be unique within the file. |
email | No | Recipient email, for your records and notifications. |
name | No | Recipient display name. |
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.
| Code | When |
|---|---|
invalid_address | The recipient is not a valid Solana address. |
missing_recipient | The recipient cell is empty. |
missing_amount | The amount cell is empty. |
zero_amount | The amount is zero. |
invalid_amount | Not a plain decimal number (no signs, exponents or currency symbols). |
too_many_decimals | More decimal places than MUNY supports. |
duplicate_reference | The same reference appears on more than one row. |
duplicate_row | An exact duplicate row without a reference. |
malformed_csv | Unterminated quotes, wrong column count, or an unreadable file. |
missing_column | The 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
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.
{
"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-failedre-queues only the failed payouts — confirmed payouts are never sent again. GET /v1/payout-batches/:id/exportreturns 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.
In the dashboard#
Upload
Drop a CSV on Payouts → Bulk payout.Fix errors
Every invalid row is listed with its line number and reason.Review
Check recipients, total MUNY required and the funding wallet.Confirm
The batch is created and processed in the background with live progress.