Bulk transfers
Pay many recipients with one submission, once or on a schedule.
Bulk transfers let you build a list of recipients, check them, and submit them together. This module is called bulk_transfers.
Test with an sk_test_ key first. Sandbox bulk transfers run on your sandbox balance and pay simulated recipients, so no real money moves.
Send a bulk transfer
Create a draft
account_id is the account to debit, and a merchant_reference lets you retry safely. To pay everyone the same amount, also send use_same_amount: true and same_amount.
curl https://api.payinto.co/api/v1/business/bulk-transfers \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"account_id": "01964316-57cb-7284-8651-c53d3fcee694",
"name": "October vendor payouts",
"merchant_reference": "payouts-2026-10"
}'Add and resolve recipients
Add each recipient, then resolve them to confirm the account name. A bank code and account number can appear only once. Check resolved_account_name before you submit.
curl https://api.payinto.co/api/v1/business/bulk-transfers/$BULK_ID/items \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"bank_code": "058",
"bank_name": "Guaranty Trust Bank",
"account_number": "0123456789",
"amount": 250000,
"narration": "October invoice"
}'curl --request POST https://api.payinto.co/api/v1/business/bulk-transfers/items/$ITEM_ID/resolve \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Accept: application/json"Submit
Your available balance must cover the requested amount and expected fees.
curl --request POST https://api.payinto.co/api/v1/business/bulk-transfers/$BULK_ID/submit \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Accept: application/json"The status becomes queued. If your business uses an approval workflow, it becomes awaiting_approval until someone approves it in the dashboard.
Submitting again is safe. You get the current state and the X-Idempotency-Replay: true header, and nobody is paid twice.
Track the result
Subscribe to the bulk_transfer.finished webhook, which is sent when every recipient has reached a final status. You can also call GET /bulk-transfers/{id} for each recipient's status and attempts, or GET /bulk-transfers/{id}/export for a CSV.
If some recipients fail, the status is partially_successful. Fix the cause, then call POST /bulk-transfers/{id}/retry to try only the failed recipients again.
Import recipients from a file
To add many recipients at once, upload a CSV or XLSX file of up to 5 MB. Download the columns from GET /bulk-transfers/template.
Import amounts are in naira
Unlike the rest of the API, amounts in an import file are in naira, not kobo. 2500 in a file means ₦2,500.00.
Check a file first with POST /bulk-transfers/{id}/import/preview, which doesn't change the draft. Then send the same file to the import endpoint:
curl https://api.payinto.co/api/v1/business/bulk-transfers/$BULK_ID/import \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Accept: application/json" \
--form "[email protected]"Valid rows are added, and each invalid row is listed under errors. Imported recipients still need to be resolved.
Reuse a bulk transfer
POST /bulk-transfers/{id}/duplicate creates a new draft with the same recipients. Each recipient must be resolved again.
Statuses
| Status | Meaning |
|---|---|
draft | You can still change it. |
awaiting_approval | Waiting for approval in the dashboard. |
queued, processing | Payments are in progress. |
completed | Every recipient was paid. |
partially_successful | Some recipients were paid, and some failed. |
failed | No recipient was paid. |
Only draft and ready bulk transfers can be edited. Recipients can only change while the status is draft, and only drafts can be deleted.
Schedule a bulk transfer
Create a schedule from a bulk transfer whose recipients are all resolved. The first run must be at least 10 minutes away. Keep that bulk transfer, because live runs use it as a template.
curl https://api.payinto.co/api/v1/business/scheduled-bulk-transfers \
--header "Authorization: Bearer $PAYINTO_SECRET_KEY" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"source_account_id": "01964316-57cb-7284-8651-c53d3fcee694",
"bulk_transfer_id": "0196431d-7a10-7e22-9c41-6b2d8f30c955",
"merchant_reference": "monthly-vendors",
"schedule": {
"frequency": "monthly",
"interval": 1,
"starts_at": "2026-12-01T09:00:00+01:00",
"timezone": "Africa/Lagos",
"ends_type": "occurrences",
"max_occurrences": 12
}
}'| Field | Values |
|---|---|
frequency | once, daily, weekly, monthly, yearly, custom |
ends_type | never, date, occurrences |
weekdays | For weekly schedules, days from 0 (Sunday) to 6 (Saturday) |
month_day | For monthly schedules, 1 to 31 |
custom_unit | days, weeks, months, years |
If your business requires approval, the response is 202 and the status is pending_approval.
Manage a schedule
Use /pause, /resume, and /cancel to control a schedule. Schedules that are active or paused can be edited with PATCH.
Check each run
Each run creates a new bulk transfer and sends its own bulk_transfer.finished event. GET /scheduled-bulk-transfers/{id}/executions lists the runs, and each one has the bulk_transfer_id it created. You can also call GET /bulk-transfers?scheduled_payment_id={id}.
Good to know
- Limits apply to the whole submission. The total is checked against your account's daily debit limit, and each transfer is checked again when it's processed.
- Requests act as the business owner. Payments and audit records created with your API key are attributed to the owner, so your business needs an active owner.
- Sandbox fees are simulated. Live fees can differ.