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/batch-payout-requests endpoint accepts an array of up to 50 payout requests and submits them all at once. NextAPI processes each payout independently:

  • Each payout runs on the rail you specify — set settlement_rail per item
  • 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 UUID 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/YOUR_ACCOUNT_UUID/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, sourceAccountId, payPeriod) {
return employees.map(emp => ({
source_account_id: sourceAccountId,
external_id: `payroll-${payPeriod}-${emp.employeeId}`,
amount_cents: emp.netPayCentavos,
currency: 'PHP',
settlement_rail: emp.netPayCentavos < 5000000 ? 'instapay' : 'pesonet',
status: 'published',
settlement_account: {
bank_account_name: emp.accountName,
bank_account_number: emp.bankAccountNumber,
bank_code: emp.bankBicCode,
},
description: `Salary ${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/batch-payout-requests" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: payroll-nov-2025-v1" \
-d '[
{
"source_account_id": "YOUR_ACCOUNT_UUID",
"external_id": "payroll-nov-2025-EMP001",
"amount_cents": 2500000,
"currency": "PHP",
"settlement_rail": "pesonet",
"status": "published",
"settlement_account": {
"bank_account_name": "Juan dela Cruz",
"bank_account_number": "1234567890",
"bank_code": "BOPIPHMMXXX"
},
"description": "Salary Nov 2025 - EMP001"
},
{
"source_account_id": "YOUR_ACCOUNT_UUID",
"external_id": "payroll-nov-2025-EMP002",
"amount_cents": 3200000,
"currency": "PHP",
"settlement_rail": "pesonet",
"status": "published",
"settlement_account": {
"bank_account_name": "Maria Santos",
"bank_account_number": "09171234567",
"bank_code": "GCSHPHM2XXX"
},
"description": "Salary Nov 2025 - EMP002"
}
]'

Response

{
"batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"total_count": 2,
"total_amount": 5700000,
"payout_requests": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"external_id": "payroll-nov-2025-EMP001",
"status": "published",
"amount_cents": 2500000
},
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"external_id": "payroll-nov-2025-EMP002",
"status": "published",
"amount_cents": 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.processed': {
const { id, reference, amount } = event.data;
// reference is what you set — use to match employee
await markEmployeePaid(reference, { payoutRequestId: id, amount });
break;
}

case 'payout.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/YOUR_PAYOUT_REQUEST_UUID"

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.external_id}-retry-${suffix}`,
},
body: JSON.stringify({
source_account_id: failedPayout.source_account_id,
external_id: `${failedPayout.external_id}-retry-${suffix}`,
amount_cents: failedPayout.amount_cents,
currency: failedPayout.currency,
settlement_rail: failedPayout.settlement_rail,
status: 'published',
settlement_account: failedPayout.settlement_account,
description: failedPayout.description,
}),
}
);
return response.json();
}

→ See Handle Payout Failures for the full failure classification and retry strategy.

Large batches

Each batch call accepts a maximum of 50 items. For payrolls over 50 recipients, split into multiple 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, 50);

for (let i = 0; i < chunks.length; i++) {
const payoutRequests = buildPayoutBatch(chunks[i], accountId, payPeriod);
await fetch('https://api.partners.nextpay.world/v2/batch-payout-requests', {
method: 'POST',
headers: {
'Authorization': 'Basic ...',
'Content-Type': 'application/json',
'X-Idempotency-Key': `payroll-${payPeriod}-batch-${i}-v1`,
},
body: JSON.stringify(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/health/{service_name} returns current InstaPay and PESONet availability.