Skip to main content

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 -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"
}
]
}'

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-health returns current InstaPay and PESONet availability.