Skip to content
MultigateAPI
Support Staff

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_ID and MG_ACCOUNT_NUMBER are set, as in the Quickstart.
  • Your API user has the initiate payment permission (can_initiate_payment). Approve payment alone isn't enough to upload.
  • You know who pays: onBehalfOf is 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
Get the template
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.

Fill it in

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.okafor@example.com
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.

Preview it

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.openxmlformats-officedocument.spreadsheetml.sheet 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 400 with the first problem only. Fix it and upload again.
  • Row problems come back in rows[].errors with field and message, for example beneficiaryEmailAddress "Beneficiary email address is invalid." or beneficiaryDetails "Beneficiary lookup failed.". rowNumber is 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.

Fix row errors, if any

A preview with any row error can't be committed. To fix them:

  1. 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" }
  ]
}
  1. For each row, correct the cell that field points to (rowNumber is the line in your file, header = line 1):
field Fix
beneficiaryDetails The bank couldn't confirm the account. Check BENEFICIARY ACCOUNT NUMBER and BENEFICIARY BANK BIC OR CODE with Look up an account name.
beneficiaryEmailAddress 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).
intermediaryBank, intermediaryBankCountry, 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.
sourceAccountNumber The row pays the source account itself. Remove or correct the row.
  1. Or remove the rows you can't fix yet, and pay them later in another file.
  2. 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.
Commit it
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.

One commit at a time

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.

Track the rows

Every row is an ordinary transfer with its own uniqueRequestId (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 (reference is the row's NARRATION, shortened).
  • Pages hold at most 100 rows: increase pageNumber until you reach totalPages.
  • Then handle each row's transfer_completed / transfer_failed webhook by its uniqueRequestId, as in Payouts › After you send, and poll rows that stay non-final with the same batchId query.

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 responseMessage Meaning
01 Transfers sent successfully Rows created as pending and paid in the background.
42 Transfers submitted for approval Rows wait as pending_approval 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_approval 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_approval 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_completed or transfer_failed per row. Rows declined by an approver send no webhook.
  • 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_approval doesn't return bulk rows. Use batchId instead.
  • Supporting documents for third-party USD rows go in assets on 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.ms-excel or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet).
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 sourceAccountNumber 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