Products · Payouts

Bulk Disbursements

Upload a CSV or call the API to fan out payments to thousands of recipients across Waafi, D-Money, EAB and CAC. 4-eyes approval baked in.

5 endpointspo_

How it works

  1. 1

    Create a payout batch

    POST to /v1/payouts/bulk with either a multipart CSV upload or a JSON rows array. Each row specifies a recipient, amount, provider, and a per-row reference that acts as the idempotency key for that individual payout.
  2. 2

    Batch enters pending_approval

    The batch is created in pending_approval status and cannot disburse until a second authorised user approves it. The creator cannot self-approve — this is enforced at the API level, not just the portal.
  3. 3

    Second user approves

    POST to /v1/payouts/bulk/{id}/approve using an API key belonging to a different principal. Attempting to approve with the same key that created the batch returns 403 self_approval_not_allowed.
  4. 4

    Batch fans out

    On approval the orchestrator fans out each row in parallel. Every row becomes an individual po_ payout record, routed to the correct provider rail. Row-level failures are isolated — a failed row does not block others.
  5. 5

    Webhooks fire per row

    payout.created, payout.succeeded, and payout.failed fire for each individual payout row. The batch-level object transitions to completed once all rows have reached a terminal state.

CSV format

When uploading a file, use UTF-8 encoding with a header row. Column order is fixed; all five columns are required.

ColumnFormatNotes
recipient_msisdnE.164 stringUse for mobile-money providers. Mutually exclusive with recipient_account.
recipient_accountstringBank account identifier. Mutually exclusive with recipient_msisdn.
amountintegerDJF minor units (DJF is zero-decimal — pass whole DJF, e.g. 5000 = DJF 5,000).
providerenumwaafi · dmoney · eab · cac
referencestringYour unique per-row reference. Used as idempotency key for that individual payout.
descriptionstringHuman-readable label. Appears on receipts and in the portal. ≤140 chars.

Endpoints

POST/v1/payouts/bulk

Create a new payout batch (JSON rows or multipart CSV).

GET/v1/payouts/bulk/{id}

Retrieve a batch and its aggregate status.

POST/v1/payouts/bulk/{id}/approve

Approve a pending_approval batch. Requester ≠ approver enforced.

POST/v1/payouts/bulk/{id}/cancel

Cancel a batch before any rows have been processed.

GET/v1/payouts

List individual po_ payout rows. Filter by batch_id, status, provider.

Create parameters

NameTypeDescription
rows
required
arrayArray of payout row objects when using JSON. Omit when uploading a CSV file via multipart.
currency
required
ISO 4217Currency for all rows in this batch. Currently only "DJF" is supported for bulk payouts.
descriptionstringBatch-level label for portal and reporting. ≤255 chars.
rows[].recipient_msisdnE.164Mobile number of the recipient. Required for waafi and dmoney.
rows[].recipient_accountstringBank account identifier. Required for eab and cac.
rows[].amount
required
integerAmount in DJF minor units.
rows[].provider
required
enumProvider for this row: waafi · dmoney · eab · cac.
rows[].reference
required
stringYour unique reference for this row. Idempotent — re-submitting the same reference within a batch is a no-op.
rows[].descriptionstringRow-level description. Appears on the recipient's statement where the provider supports it.

4-eyes enforcement

The API key that created the batch cannot also approve it. Use separate keys for the requester and approver roles, or have a second authorised user approve in the merchant portal. Attempting to self-approve returns 403 self_approval_not_allowed.

Limits

LimitDefaultIncrease available
Rows per batch10,000Yes — contact support
Total batch valueDJF 50,000,000Yes — requires additional approval tier
Concurrent pending batches5Yes — contact support
CSV file size10 MBNo

Example — create and approve a batch

POST /v1/payouts/bulkbash
curl -X POST https://api.merashub.com/v1/payouts/bulk \
  -H "Authorization: Bearer sk_live_requester_..." \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "DJF",
    "description": "May 2026 agent commissions",
    "rows": [
      {
        "recipient_msisdn": "+25377000001",
        "amount": 12500,
        "provider": "waafi",
        "reference": "comm_may_agt_001",
        "description": "Commission payout — agent 001"
      },
      {
        "recipient_msisdn": "+25377000002",
        "amount": 9800,
        "provider": "dmoney",
        "reference": "comm_may_agt_002",
        "description": "Commission payout — agent 002"
      }
    ]
  }'
201 Created — pending_approvaljson
{
  "id": "batch_01HZGK3MNP7Q2W9RX5YE8TF4JD",
  "status": "pending_approval",
  "currency": "DJF",
  "row_count": 2,
  "total_amount_minor": 22300,
  "description": "May 2026 agent commissions",
  "created_at": "2026-05-25T11:00:00Z",
  "created_by": "key_requester_..."
}
POST /v1/payouts/bulk/{id}/approvebash
curl -X POST https://api.merashub.com/v1/payouts/bulk/batch_01HZGK3MNP7Q2W9RX5YE8TF4JD/approve \
  -H "Authorization: Bearer sk_live_approver_..."
  # Must be a different key from the one that created the batch
200 OK — processingjson
{
  "id": "batch_01HZGK3MNP7Q2W9RX5YE8TF4JD",
  "status": "processing",
  "approved_at": "2026-05-25T11:04:21Z",
  "approved_by": "key_approver_...",
  "row_count": 2,
  "rows_succeeded": 0,
  "rows_failed": 0,
  "rows_pending": 2
}

Handling failed rows

Failed individual payouts within a batch do not block others. Check payout.failed webhooks and retry specific po_ IDs via POST /v1/payouts/{id}/retry. Each retry reuses the original row reference as the idempotency key, so duplicate retries are safe.