Skip to main content
A bulk NGN withdrawal pays several bank accounts from one signed request. You send one list of beneficiaries, Stabyl creates one parent withdrawal, and every beneficiary settles as its own bank transfer. This is the right tool for payroll, supplier runs, customer refunds, and any other job where you would otherwise loop over single withdrawals. Bulk uses the same routes, signing, and scopes as a single withdrawal. If you have not read Single withdrawals yet, start there. This page only covers what changes when items has more than one entry.
Bulk withdrawals are available for NGN bank payouts today. A USD stablecoin withdrawal accepts exactly one item per request; sending more returns 400 VALIDATION_FAILED.

How it works

  1. Build one items list with one entry per beneficiary.
  2. Quote it with POST /partner/wallets/withdrawal/quotes to preview the total debit and the number of bank transfers.
  3. Submit the same list with POST /partner/wallets/withdrawals and one idempotency_key.
  4. Persist the returned parent id, then reconcile each transfer with GET /partner/wallets/transactions/{id}.
The whole request is checked before any money moves. If one beneficiary is invalid, the request is rejected and nothing is debited. Once accepted, every transfer settles on its own, so one bank rejecting a payment does not undo the others.

Limits

The account limit and balance check apply to the whole request, not to each beneficiary. A request of 50 small payments and a request of one large payment are both measured by their total. The account limit counts principal only; the balance check counts principal plus the ₦100 fee per transfer. The item, transfer, and per-transfer amount limits are server settings that can change, so treat them as current values rather than permanent constants. Because kobo is dropped after the request is accepted, send whole-naira amounts; two requests that differ only in kobo are different intents under the same idempotency key.

Fees

The NGN withdrawal fee is a flat ₦100.00 for every bank transfer that is created. Most beneficiaries need exactly one transfer, so for a typical bulk request the fee is ₦100.00 multiplied by the number of beneficiaries. A beneficiary above ₦100,000,000.00 is split into more than one transfer and pays the fee for each of them. The quote response reports transfer_count, the aggregate fee, and the debit_amount that will leave your NGN wallet, so you can show the true cost before you submit.

Quote a bulk request

Quote bodies use items and omit idempotency_key:
The quote for this list reports three transfers, a fee of 300.00, an amount of 1045000.00, and a debit_amount of 1045300.00. Quoting never reserves funds, but it runs the account-limit and balance checks and has its own rate limit, so quote when the operator reviews the list rather than on every keystroke.

Submit a bulk request

Submit uses the same list and adds one idempotency_key for the whole batch. Sign the exact bytes you send, as described in Request Signing.
Use one idempotency_key per batch, not per beneficiary. Give every item a client_item_id from your own system. It is optional, but it is the easiest way to match a transfer outcome back to the right employee, supplier, or customer, and it comes back in the submit response and in every later transaction read. The same bank account can appear more than once in a list. Each entry is paid separately, so check your list for accidental duplicates before you submit.

Submit response

The response is one parent transaction. items keeps the order of your request, and each item lists the bank transfers that will pay it.
sequence numbers run across the whole batch, so the first transfer of the second item continues counting from where the first item stopped. The parent starts as pending and each transfer as processing. Store data.id immediately. It is the only identifier you need to follow the batch to completion.

Outcomes

Each bank transfer succeeds or fails on its own. The parent status summarizes them: When a transfer fails, its principal and its ₦100.00 fee are returned to your NGN wallet. Transfers that succeeded stay paid. You never need to reverse or re-send the successful part of a batch; only re-send the beneficiaries whose transfers failed, in a new request with a new idempotency_key. Each outcome has its own webhook: withdrawal.success, withdrawal.partial, and withdrawal.failed. Subscribe to all three so a mixed batch is not left waiting for an event that will never arrive. See Withdrawal webhook events for the payloads.
A webhook tells you the batch has finished. It does not list who was paid. Finish bulk reconciliation with the transaction read below, which carries the per-transfer outcomes.

Reconciling each transfer

Read the parent with GET /partner/wallets/transactions/{id}. The settlement_details.transfers list carries one entry per bank transfer with its own status, your client_item_id, and the position of the item in your original request.
The parent-level recipient and description show the first beneficiary in the batch. Use the per-transfer recipient and client_item_id for anything beneficiary specific. description and recipient on a transfer are omitted rather than null when absent. On a partial parent, confirmed_at is the time the batch finished settling and failed_at stays null. settlement_details.rail_reference carries the same WD- reference as reference. In the example above, two employees were paid and one transfer failed. Your system should mark the first and third employees as paid and queue a fresh withdrawal for the second employee after checking the account details. The wallet receives 420100.00 back in total: a refund transaction for the 420000.00 principal appears in the timeline with the description Refund for failed withdrawal transfers, and the 100.00 fee for that transfer is released back to the balance alongside it.

Retries and idempotency

A transport failure on submit is handled exactly like a single withdrawal. Keep the same body and idempotency_key, generate a new timestamp and nonce, sign again, and resubmit. The same key with the same list returns the existing parent instead of paying anyone twice. Changing anything in the list under the same key returns IDEMPOTENCY_CONFLICT. That includes the order of the items, an amount, an account number, a description, or a client_item_id. If your list changed, it is a new batch and needs a new key.

Practical tips

  • Quote first and show the transfer_count and debit_amount to the person approving the batch.
  • Keep batches at 50 beneficiaries or fewer and split larger payroll runs into several requests with distinct idempotency keys such as payroll-2026-09-run-1, payroll-2026-09-run-2.
  • Save client_item_id alongside your own record before you submit, so a restart between submit and reconciliation cannot lose the mapping.
  • Treat partial as final. Retry only the failed beneficiaries, never the whole batch.
  • Subscribe to withdrawal.partial alongside withdrawal.success and withdrawal.failed, or a mixed batch will never trigger your webhook handler.
  • Reconcile every batch with a transaction read even when a webhook arrived. Webhooks tell you the batch finished; the transaction read tells you who was paid.