Products
Bulk payouts
Pay many beneficiaries in one go (payroll, supplier runs, marketplace settlements) by uploading a spreadsheet. You preview the whole file first, fix any row errors, then commit it once. Each row then becomes an ordinary transfer with its own status and webhook.
A bulk payout happens in two calls:
- Preview: you upload the file. Multigate checks every row and tells you what's wrong, without moving money.
- Commit: you confirm the preview. Multigate creates one transfer per row (a batch) and pays them in the background.
Before you start
-
MG_BASE_URL,MG_API_KEY,MG_CLIENT_IDandMG_are set, as in the Quickstart.ACCOUNT_ NUMBER - Your API user has the initiate payment permission (
can_). Approve payment alone isn't enough to upload.initiate_ payment - You know who pays:
onBehalfOfis required on the preview. Send your client ID (MG_CLIENT_ID), or a customer ID when you pay from a customer's account. - One file pays from one source account, in that account's currency. Every row is paid in that currency.
- The source account will hold the total of all rows plus fees when you commit. The preview tells you the exact total.
- For USD rows paying someone other than the account holder, you have supporting documents uploaded and their IDs ready for
assets(ask Multigate how to upload them). - You've read Payouts: each row is paid exactly like a single transfer.
How it works
sequenceDiagram
autonumber
participant You as Your backend
participant MG as Multigate
participant Bank as Beneficiary banks
You->>MG: GET /transfer/bulk-transfer-template
MG-->>You: Template (xlsx or csv)
You->>MG: POST /transfer/bulk-transfer/preview (file)
MG->>Bank: Look up each beneficiary
MG-->>You: previewId, batchId, totals, row errors
Note over You: Fix errors and preview again until no row has errors
You->>MG: POST /transfer/bulk-transfer/commit (previewId) within 30 minutes
MG-->>You: batchReference, responseCode 01 or 42
loop Each row, in the background
MG->>Bank: Payment
MG-)You: transfer_completed or transfer_failed
end
curl -o bulk-template.csv "$MG_BASE_URL/payment-service/api/v1/transfer/bulk-transfer-template?format=csv" \
-H "mp-api-key: $MG_API_KEY"
Leave out format=csv to get the Excel workbook. Only its Transfer Entries sheet is read. The Instruction sheet explains each column; the other sheets list bank names with BICs, countries and currencies for the drop-downs.
One transfer per row. Keep the header row exactly as in the template (same spelling, upper case and spaces). Empty rows are skipped.
| Column | Required | What to put in it | Example (NGN row) |
|---|---|---|---|
| AMOUNT | Yes | Amount in major units of the source currency. No thousands separators or currency symbols. | 400000 |
| NARRATION | Yes | Payment description. The beneficiary sees the first 36 characters. | October salary |
| BENEFICIARY BANK | Yes | Bank name. | Guaranty Trust Bank |
| BENEFICIARY BANK BIC OR CODE | Yes | Local bank code (bankCode from List banks) for local rows, or the SWIFT BIC for SWIFT rows. |
058 |
| BENEFICIARY BANK ADDRESS | Yes | The bank's address. | 635 Akin Adesola Street, Lagos |
| BENEFICIARY ACCOUNT NAME | Yes | Name on the account, ideally as returned by Look up an account name. | ADA OKAFOR |
| BENEFICIARY ACCOUNT NUMBER | Yes | Digits only. NGN: exactly 10 digits. Keep leading zeros: in Excel, format the column as text. An account number with letters (such as an IBAN) is refused with "Invalid data type". | 0123456789 |
| BENEFICIARY NAME | Yes | The beneficiary's full or business name. | Ada Okafor |
| BENEFICIARY ADDRESS | Yes | Street address. | 12 Marina Road |
| BENEFICIARY CITY | Yes | Lagos |
|
| BENEFICIARY STATE | Yes | Lagos |
|
| BENEFICIARY COUNTRY | Yes | NG, NGA, or the drop-down value such as NIGERIA - NG. |
NGA |
| BENEFICIARY EMAIL ADDRESS | Yes | A valid email address. | ada. |
| BENEFICIARY PHONE NUMBER | No | Digits, optionally starting with +. No spaces or dashes. |
2348012345678 |
| INTERMEDIARY BANK | SWIFT rows | The drop-down value Bank name-BIC, or the BIC alone. Leave empty for local rows. |
(empty) |
| INTERMEDIARY BANK COUNTRY | SWIFT rows | Same formats as BENEFICIARY COUNTRY. | (empty) |
| CHARGE TYPE | SWIFT rows | Who pays SWIFT bank charges: OUR (you), SHA (shared) or BEN (the beneficiary). |
(empty) |
| PAYMENT METHOD | Yes | The rail: NIP for NGN rows, SWIFT for SWIFT rows. See List payment methods. |
NIP |
| PURPOSE CODE | No | Not in the template, but read if you add the column. A code from List purpose codes. | (column not added) |
A complete CSV file with two NGN rows looks like this:
AMOUNT,NARRATION,BENEFICIARY BANK,BENEFICIARY BANK BIC OR CODE,BENEFICIARY BANK ADDRESS,BENEFICIARY ACCOUNT NAME,BENEFICIARY ACCOUNT NUMBER,BENEFICIARY NAME,BENEFICIARY ADDRESS,BENEFICIARY CITY,BENEFICIARY STATE,BENEFICIARY COUNTRY,BENEFICIARY EMAIL ADDRESS,BENEFICIARY PHONE NUMBER,INTERMEDIARY BANK,INTERMEDIARY BANK COUNTRY,CHARGE TYPE,PAYMENT METHOD
400000,October salary,Guaranty Trust Bank,058,"635 Akin Adesola Street, Lagos",ADA OKAFOR,0123456789,Ada Okafor,12 Marina Road,Lagos,Lagos,NGA,ada.okafor@example.com,2348012345678,,,,NIP
250000,October salary,Guaranty Trust Bank,058,"635 Akin Adesola Street, Lagos",TUNDE BELLO,0234567891,Tunde Bello,3 Allen Avenue,Ikeja,Lagos,NGA,tunde.bello@example.com,,,,,NIP
A SWIFT row (in a USD file) fills the last four columns, for example …,Citibank N.A.-CITIUS33XXX,USA,SHA,SWIFT. Its account number must still be digits only.
Put a value in quotes when it contains a comma, as in the bank address above.
Send the file as the multipart part file, with its content type set. Many clients send application/octet-stream, which is refused. Use text/csv for CSV and application/vnd. for .xlsx.
curl -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/bulk-transfer/preview" \
-H "mp-api-key: $MG_API_KEY" \
-F "file=@bulk-template.csv;type=text/csv" \
-F "onBehalfOf=$MG_CLIENT_ID" \
-F "sourceAccountNumber=$MG_ACCOUNT_NUMBER" \
-F "sourceCurrencyIso3Code=NGN"
{
"message": "Successful",
"previewId": "2f1c0b9a-8d7e-4f6a-9b5c-3e2d1c0b9a8d",
"batchId": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"summary": { "totalAmount": 650000, "totalFees": 100, "transferCount": 2, "requiresApproval": false },
"rows": [
{ "rowNumber": 2, "narration": "October salary", "amount": 400000, "currency": "NGN", "errors": [] },
{ "rowNumber": 3, "narration": "October salary", "amount": 250000, "currency": "NGN", "errors": [] }
]
}
Nothing is paid yet. Two kinds of problems come back:
- File problems (a missing column, an empty mandatory cell, a wrong type, an unknown purpose code) return
400with the first problem only. Fix it and upload again. - Row problems come back in
rows[].errorswithfieldandmessage, for examplebeneficiary"Beneficiary email address is invalid." orEmail Address beneficiary"Beneficiary lookup failed.".Details rowNumberis the line in your file, counting the header as line 1.
Show summary.totalAmount + summary.totalFees to your user for confirmation; that's what the rows will debit. summary.requiresApproval: true means the rows will wait for an approver in your business.
During the preview, each beneficiary is looked up at their bank, and beneficiaries you haven't paid before are saved to your beneficiary list.
A preview with any row error can't be committed. To fix them:
- List the rows that have errors:
curl -s -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/bulk-transfer/preview" \
-H "mp-api-key: $MG_API_KEY" \
-F "file=@bulk-template.csv;type=text/csv" \
-F "onBehalfOf=$MG_CLIENT_ID" \
-F "sourceAccountNumber=$MG_ACCOUNT_NUMBER" \
-F "sourceCurrencyIso3Code=NGN" \
| jq '.rows[] | select(.errors | length > 0) | {rowNumber, errors}'
{
"rowNumber": 3,
"errors": [
{ "field": "beneficiaryEmailAddress", "message": "Beneficiary email address is invalid." },
{ "field": "beneficiaryDetails", "message": "Beneficiary lookup failed.", "code": "beneficiaryLookupFailed" }
]
}
- For each row, correct the cell that
fieldpoints to (rowNumberis the line in your file, header = line 1):
field |
Fix |
|---|---|
beneficiary |
The bank couldn't confirm the account. Check BENEFICIARY ACCOUNT NUMBER and BENEFICIARY BANK BIC OR CODE with Look up an account name. |
beneficiary |
Correct BENEFICIARY EMAIL ADDRESS. |
amount |
Change AMOUNT to fit the currency's limits ("Minimum amount not met", "Maximum amount exceeded"). |
paymentMethod |
Use a PAYMENT METHOD configured for the currency and country (List payment methods). |
intermediary, intermediary, chargeType |
Fill those columns for SWIFT rows. |
assets |
A USD row pays a third party: send supporting document IDs in assets. |
charge |
The fee couldn't be calculated. Preview again in a few minutes. |
source |
The row pays the source account itself. Remove or correct the row. |
- Or remove the rows you can't fix yet, and pay them later in another file.
- Preview the corrected file again. You get a new
previewId; commit that one. Previews you don't commit expire after 30 minutes and are never paid.
curl -X POST "$MG_BASE_URL/payment-service/api/v1/transfer/bulk-transfer/commit" \
-H "mp-api-key: $MG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "previewId": "2f1c0b9a-8d7e-4f6a-9b5c-3e2d1c0b9a8d", "comment": "October payroll" }'
{
"message": "Successful",
"batchReference": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"processing": 2,
"failed": 0,
"responseCode": "01",
"responseMessage": "Transfers sent successfully"
}
Commit within 30 minutes of the preview. The balance check (total plus fees) happens now. A preview can be committed only once: after a successful commit, a second one returns "Invalid transaction…" and creates nothing.
Don't send two commits for the same previewId at once. If a commit times out, list the batch with Find transfers ?batchId= (the preview's batchId) before you try again.
Every row is an ordinary transfer with its own unique (generated by Multigate), transactionId, status and webhook. The webhook doesn't carry the batchId, so right after the commit, list the rows and store each row's IDs against your own records:
curl -s "$MG_BASE_URL/payment-service/api/v1/transaction/report?batchId=6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d&pageSize=100&pageNumber=1" \
-H "mp-api-key: $MG_API_KEY" \
| jq '{totalPages, rows: [.data[] | {uniqueRequestId, transactionId, status, amount, account: .beneficiaryDetails.accountNumber, reference}]}'
- Match each row to your file by beneficiary account number and amount (
referenceis the row's NARRATION, shortened). - Pages hold at most 100 rows: increase
pageNumberuntil you reachtotalPages. - Then handle each row's
transfer_/completed transfer_webhook by itsfailed unique, as in Payouts › After you send, and poll rows that stay non-final with the sameRequest Id batchIdquery.
Users with the approve payment permission can also see the batch with its totals and all rows with Get a bulk transfer.
Commit results
responseCode |
response |
Meaning |
|---|---|---|
01 |
Transfers sent successfully | Rows created as pending and paid in the background. |
42 |
Transfers submitted for approval | Rows wait as pending_ until an approver in your business acts in the dashboard. |
03 |
Transaction failed | No row could be sent. |
failed counts rows created as failed because the beneficiary couldn't be confirmed. It's normally 0, because such rows show up as preview errors and block the commit.
Statuses
Each row follows the transfer statuses: pending or pending_ after commit, then processing, then completed or failed. An approver can decline a waiting row (rejected, no webhook).
The batch has its own summary status:
| Batch status | Meaning |
|---|---|
pending_ |
At least one row waits for an approver. |
processing |
Rows are being paid. |
completed |
Every row is final and at least one was paid. Some rows may still have failed. |
failed |
Every row is final and none was paid. |
rejected |
An approver declined every row. |
stateDiagram-v2
direction LR
[*] --> pending_approval: needs approval
[*] --> processing: sent
pending_approval --> processing: approved
pending_approval --> rejected: all declined
processing --> completed: every row final, at least one paid
processing --> failed: every row final, none paid
completed --> [*]
failed --> [*]
rejected --> [*]
A batch is completed as soon as one row is paid and the rest are final, so check each row's status, not only the batch.
Things to know
- There is no batch-level webhook. You receive one
transfer_orcompleted transfer_per row. Rows declined by an approver send no webhook.failed - Nothing is reserved at commit. Each row is debited when it's paid, so spending from the same account in the meantime can make later rows fail.
- Bulk rows are never split, and the preview doesn't flag an NGN NIP row above the per-payment maximum. Keep each such row within the maximum shown by List payment methods.
- Large files: the preview looks up every beneficiary and must finish within 60 seconds at the gateway. Split very large runs into several files.
- Finding waiting rows: Find transfers with
status=pending_doesn't return bulk rows. Useapproval batchIdinstead. - Supporting documents for third-party USD rows go in
assetson the preview, as IDs. They apply to the whole batch. - Emails: your API user gets a "Transfer Notification" email per row when it starts processing and when it completes or fails.
- The Nigerian account number rule applies to every row.
Errors
| When | Message | Fix |
|---|---|---|
| Preview | File is required | Send the file as the multipart part file. |
| Preview | Invalid file type. … are allowed. | Set the part's content type (text/csv, application/csv, application/vnd. or application/vnd.). |
| Preview | Missing mandatory column {name} | Use the template's header row unchanged. |
| Preview | Row {n}: Missing mandatory field {name} | Fill that cell. "Row : Missing mandatory field" with no number means the file has no data rows. |
| Preview | Row {n}: Invalid data type for field {name}. Must be a number. | Remove letters, spaces, commas or dashes from AMOUNT, BENEFICIARY ACCOUNT NUMBER or BENEFICIARY PHONE NUMBER. |
| Preview | Invalid Purpose Code | Use a code from List purpose codes, or leave the cell empty. |
| Preview | Source Account Not Found / Invalid Source Account Owner | Check source and that it belongs to onBehalfOf. |
| Preview (row) | Beneficiary lookup failed. | Check the account number and bank code with Look up an account name. |
| Preview (row) | Minimum amount not met / Maximum amount exceeded | Change the amount. |
| Preview (row) | Intermediary bank / Intermediary bank country / Charge type is required for SWIFT transfers. | Fill those columns for SWIFT rows. |
| Preview (row) | Supporting documents are required for 3rd party USD transfers. | Upload documents and send their IDs in assets. |
| Commit | Invalid transaction (general response for invalid transactions) | The preview is unknown, expired (30 minutes) or already committed. Check Find transfers with the batchId before previewing again. |
| Commit | Transfer instructions are invalid, please upload a valid file | A row still has errors. Fix and preview again. |
| Commit | Insufficient balance in source account | Fund the account for the total plus fees, then commit again within the 30 minutes. |
Next
- Payouts for how each row is paid.
- Event catalogue for the row webhooks.
- Idempotency for retries.