Run Mass Payroll
This guide shows you how to send payouts to hundreds or thousands of recipients in a single batch operation — payroll, commission disbursements, supplier payments, or any scenario requiring bulk transfers.
How batch payouts work
The POST /v2/payout-requests/batch endpoint accepts an array of payout requests and submits them all at once. NextAPI processes each payout independently:
- Each payout routes to the optimal rail (InstaPay ≤ PHP 50,000; PESONet for larger amounts)
- Each payout has its own lifecycle — one failure doesn't block others
- Each completed or failed payout fires its own webhook
Prerequisites
- A source account (
acct_xxx) with sufficient available balance - Recipient bank codes from
GET /v2/receiving-institutions - A registered webhook endpoint for payout status updates
- Sandbox credentials from /sandbox
Step 1: Check available balance
Before submitting a batch, confirm the source account has enough funds:
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
"https://api.partners.nextpay.world/v2/accounts/acct_01HXYZ/balances"
{
"available": 150000000,
"pending": 2500000,
"reserved": 0,
"currency": "PHP"
}
available: 150000000 = PHP 1,500,000.00 available. Only available balance can be disbursed.
Step 2: Prepare your recipient list
Each item in the batch is a standard payout request. Construct your payload from your payroll system:
function buildPayoutBatch(employees, accountId, payPeriod) {
return employees.map(emp => ({
account_id: accountId,
amount: emp.netPayCentavos, // amounts in centavos
recipient: {
type: 'bank_account',
bank_code: emp.bankCode, // e.g., "BPI", "BDO", "GCSH" for GCash
account_number: emp.bankAccountNumber,
account_name: emp.accountName,
},
description: `Salary ${payPeriod} - ${emp.employeeId}`,
reference: `payroll-${payPeriod}-${emp.employeeId}`,
}));
}
Get valid bank codes from the institutions endpoint:
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
"https://api.partners.nextpay.world/v2/receiving-institutions"
Step 3: Submit the batch
- cURL
- Node.js
- Python
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-X POST "https://api.partners.nextpay.world/v2/payout-requests/batch" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: payroll-nov-2025-v1" \
-d '{
"payout_requests": [
{
"account_id": "acct_01HXYZ",
"amount": 2500000,
"recipient": {
"type": "bank_account",
"bank_code": "BPI",
"account_number": "1234567890",
"account_name": "Juan dela Cruz"
},
"description": "Salary Nov 2025 - EMP001",
"reference": "payroll-nov-2025-EMP001"
},
{
"account_id": "acct_01HXYZ",
"amount": 3200000,
"recipient": {
"type": "bank_account",
"bank_code": "GCSH",
"account_number": "09171234567",
"account_name": "Maria Santos"
},
"description": "Salary Nov 2025 - EMP002",
"reference": "payroll-nov-2025-EMP002"
}
]
}'
async function submitPayrollBatch(employees, sourceAccountId, payPeriod) {
const payoutRequests = buildPayoutBatch(employees, sourceAccountId, payPeriod);
const response = await fetch(
'https://api.partners.nextpay.world/v2/payout-requests/batch',
{
method: 'POST',
headers: {
'Authorization': 'Basic ' + Buffer.from('YOUR_CLIENT_ID:YOUR_CLIENT_SECRET').toString('base64'),
'Content-Type': 'application/json',
'X-Idempotency-Key': `payroll-${payPeriod}-v1`,
},
body: JSON.stringify({ payout_requests: payoutRequests }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Batch submission failed: ${error.message}`);
}
return response.json();
}
import requests
import base64
def submit_payroll_batch(employees, source_account_id, pay_period):
credentials = base64.b64encode(b'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET').decode()
payout_requests = build_payout_batch(employees, source_account_id, pay_period)
response = requests.post(
'https://api.partners.nextpay.world/v2/payout-requests/batch',
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/json',
'X-Idempotency-Key': f'payroll-{pay_period}-v1',
},
json={'payout_requests': payout_requests}
)
response.raise_for_status()
return response.json()
Response
{
"batch_id": "batch_01HABC",
"status": "processing",
"total_count": 2,
"total_amount": 5700000,
"payout_requests": [
{
"id": "pr_01HXYZ",
"reference": "payroll-nov-2025-EMP001",
"status": "initiated",
"amount": 2500000
},
{
"id": "pr_01HABC",
"reference": "payroll-nov-2025-EMP002",
"status": "initiated",
"amount": 3200000
}
],
"created_at": "2025-11-15T08:00:00Z"
}
Each payout request has its own ID and status. Processing happens asynchronously — you don't wait for completion.
Step 4: Handle payout webhooks
Set up your webhook handler to track individual payout completions:
app.post('/webhooks/nextapi', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nextpay-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'payout_request.completed': {
const { id, reference, amount } = event.data;
// reference is what you set — use to match employee
await markEmployeePaid(reference, { payoutRequestId: id, amount });
break;
}
case 'payout_request.failed': {
const { id, reference, error } = event.data;
await markPayoutFailed(reference, { payoutRequestId: id, error });
// Determine if retryable and add to retry queue
break;
}
}
res.status(200).send('OK');
});
Step 5: Handle failures
Not every payout succeeds on first attempt. Common causes:
- Wrong account number → permanent failure (do not retry with same details)
- Bank temporarily unavailable → transient failure (retry after delay)
- Insufficient balance → check balance and retry after funding
Check individual payout status:
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
"https://api.partners.nextpay.world/v2/payout-requests/pr_01HXYZ"
For failed payouts that are retryable, submit a new payout request with a new idempotency key:
async function retryFailedPayout(failedPayout, suffix) {
const response = await fetch(
'https://api.partners.nextpay.world/v2/payout-requests',
{
method: 'POST',
headers: {
'Authorization': 'Basic ...',
'Content-Type': 'application/json',
'X-Idempotency-Key': `${failedPayout.reference}-retry-${suffix}`,
},
body: JSON.stringify({
account_id: failedPayout.account_id,
amount: failedPayout.amount,
recipient: failedPayout.recipient,
description: failedPayout.description,
reference: `${failedPayout.reference}-retry-${suffix}`,
}),
}
);
return response.json();
}
→ See Handle Payout Failures for the full failure classification and retry strategy.
Large batches
For batches over 100 recipients, split into multiple batch calls:
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
async function runLargePayroll(employees, accountId, payPeriod) {
const chunks = chunkArray(employees, 100);
for (let i = 0; i < chunks.length; i++) {
const payoutRequests = buildPayoutBatch(chunks[i], accountId, payPeriod);
await fetch('https://api.partners.nextpay.world/v2/payout-requests/batch', {
method: 'POST',
headers: {
'Authorization': 'Basic ...',
'Content-Type': 'application/json',
'X-Idempotency-Key': `payroll-${payPeriod}-batch-${i}-v1`,
},
body: JSON.stringify({ payout_requests: payoutRequests }),
});
}
}
Key points
- Amounts in centavos. PHP 25,000.00 =
2500000. - One idempotency key per batch. If the batch request fails and you retry with the same key, NextAPI returns the original batch response without re-submitting payouts.
- Individual payout lifecycle. Each payout has its own status. One failure doesn't block others in the batch.
- Check rail health before large runs.
GET /v2/payout-requests/service-healthreturns current InstaPay and PESONet availability.
Related
- Send a Single Payout — single payout guide
- Handle Payout Failures — retry strategies
- Payout Lifecycle — state machine
- Disbursement Channels — InstaPay vs PESONet