Skip to main content

Run Payroll or Any Mass Payout

Learn how to efficiently process payroll and mass payouts using NextAPI's batch processing capabilities.

Overview

Mass payouts are essential for:

  • Payroll processing for employees
  • Refund processing to multiple customers
  • Supplier payments and vendor settlements
  • Commission distributions to partners
  • Dividend payments to shareholders

Mass Payout Architecture

Upload Recipients → Validate Data → Process Batch → Monitor Status → Handle Failures

Getting Started

Prerequisites

  • NextAPI API keys with payout permissions
  • Recipient data (account details, amounts)
  • Understanding of payout limits and compliance

Implementation Steps

1. Prepare Recipient Data

function preparePayrollData(employees) {
return employees.map(employee => ({
id: employee.id,
recipient: {
type: 'bank_account', // or 'gcash', 'maya', etc.
accountNumber: employee.bankAccount,
accountName: employee.accountName,
bank: employee.bankName
},
amount: employee.salary,
reference: `PAYROLL_${employee.id}_${Date.now()}`,
description: `Salary payment for ${employee.name}`,
metadata: {
employeeId: employee.id,
department: employee.department,
payPeriod: employee.payPeriod
}
}));
}

2. Validate Recipient Data

async function validateRecipients(recipients) {
const validationResults = [];

for (const recipient of recipients) {
const validation = {
id: recipient.id,
isValid: true,
errors: []
};

// Check required fields
if (!recipient.recipient.accountNumber) {
validation.isValid = false;
validation.errors.push('Account number is required');
}

if (!recipient.recipient.accountName) {
validation.isValid = false;
validation.errors.push('Account name is required');
}

if (!recipient.amount || recipient.amount <= 0) {
validation.isValid = false;
validation.errors.push('Amount must be greater than 0');
}

// Validate amount limits
if (recipient.amount > 1000000) { // 1M PHP limit
validation.isValid = false;
validation.errors.push('Amount exceeds single transaction limit');
}

validationResults.push(validation);
}

return validationResults;
}

3. Create Mass Payout Batch

async function createMassPayoutBatch(recipients) {
const response = await fetch('https://api.nextpay.world/v2/payouts/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
batchName: `Payroll_${new Date().toISOString().split('T')[0]}`,
payouts: recipients,
totalAmount: recipients.reduce((sum, r) => sum + r.amount, 0),
description: 'Monthly payroll processing'
})
});

if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to create batch: ${error.message}`);
}

return await response.json();
}

4. Monitor Batch Processing

class BatchProcessor {
constructor(apiKey) {
this.apiKey = apiKey;
}

async processBatch(batchId) {
console.log(`Processing batch ${batchId}...`);

let batchStatus;
let attempts = 0;
const maxAttempts = 30; // Check for 30 minutes

while (attempts < maxAttempts) {
batchStatus = await this.getBatchStatus(batchId);

console.log(`Batch status: ${batchStatus.status}`);

if (batchStatus.status === 'COMPLETED') {
console.log('Batch processing completed successfully');
return await this.getBatchResults(batchId);
}

if (batchStatus.status === 'FAILED') {
console.error('Batch processing failed');
throw new Error('Batch processing failed');
}

if (batchStatus.status === 'PARTIAL_SUCCESS') {
console.log('Batch partially completed - checking individual payouts');
return await this.handlePartialSuccess(batchId);
}

// Wait 1 minute before checking again
await new Promise(resolve => setTimeout(resolve, 60000));
attempts++;
}

throw new Error('Batch processing timeout');
}

async getBatchStatus(batchId) {
const response = await fetch(`https://api.nextpay.world/v2/payouts/batch/${batchId}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});

return await response.json();
}

async getBatchResults(batchId) {
const response = await fetch(`https://api.nextpay.world/v2/payouts/batch/${batchId}/results`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});

return await response.json();
}

async handlePartialSuccess(batchId) {
const results = await this.getBatchResults(batchId);
const failedPayouts = results.payouts.filter(p => p.status === 'FAILED');

if (failedPayouts.length > 0) {
console.log(`${failedPayouts.length} payouts failed, attempting retry...`);

// Retry failed payouts
const retryBatch = await this.retryFailedPayouts(failedPayouts);
return await this.processBatch(retryBatch.batchId);
}

return results;
}

async retryFailedPayouts(failedPayouts) {
const retryData = failedPayouts.map(payout => ({
id: payout.originalId,
recipient: payout.recipient,
amount: payout.amount,
reference: `${payout.reference}_RETRY`,
description: payout.description
}));

return await this.createMassPayoutBatch(retryData);
}
}

Complete Payroll Example

1. Full Payroll Processing

async function processPayroll(employees) {
const processor = new BatchProcessor('YOUR_API_KEY');

try {
// Step 1: Prepare data
const payrollData = preparePayrollData(employees);
console.log(`Prepared ${payrollData.length} employee payments`);

// Step 2: Validate data
const validationResults = await validateRecipients(payrollData);
const invalidRecipients = validationResults.filter(r => !r.isValid);

if (invalidRecipients.length > 0) {
console.error(`${invalidRecipients.length} recipients have validation errors`);
invalidRecipients.forEach(r => {
console.error(`Recipient ${r.id}: ${r.errors.join(', ')}`);
});

// Remove invalid recipients
const validRecipients = payrollData.filter(emp =>
validationResults.find(v => v.id === emp.id && v.isValid)
);

if (validRecipients.length === 0) {
throw new Error('No valid recipients to process');
}

payrollData.length = 0;
payrollData.push(...validRecipients);
}

// Step 3: Create batch
const batch = await createMassPayoutBatch(payrollData);
console.log(`Created batch ${batch.batchId} with ${payrollData.length} payouts`);

// Step 4: Process batch
const results = await processor.processBatch(batch.batchId);

// Step 5: Generate report
const report = generatePayrollReport(results, employees);

console.log('Payroll processing completed');
return report;

} catch (error) {
console.error('Payroll processing failed:', error);
throw error;
}
}

2. Payroll Report Generation

function generatePayrollReport(results, originalEmployees) {
const report = {
summary: {
totalEmployees: originalEmployees.length,
processedEmployees: results.payouts.length,
successfulPayments: results.payouts.filter(p => p.status === 'COMPLETED').length,
failedPayments: results.payouts.filter(p => p.status === 'FAILED').length,
totalAmount: results.payouts.reduce((sum, p) => sum + p.amount, 0)
},
successfulPayments: results.payouts
.filter(p => p.status === 'COMPLETED')
.map(p => ({
employeeId: p.metadata.employeeId,
amount: p.amount,
transactionId: p.transactionId,
processedAt: p.processedAt
})),
failedPayments: results.payouts
.filter(p => p.status === 'FAILED')
.map(p => ({
employeeId: p.metadata.employeeId,
amount: p.amount,
error: p.error,
needsRetry: true
}))
};

return report;
}

Advanced Features

1. Scheduled Payroll

const cron = require('node-cron');

async function scheduleMonthlyPayroll() {
// Run on the 25th of every month at 9 AM
cron.schedule('0 9 25 * *', async () => {
console.log('Starting scheduled payroll processing...');

try {
const employees = await getEmployeesFromDatabase();
const report = await processPayroll(employees);

// Send notification to HR
await sendPayrollNotification(report);

// Store payroll record
await storePayrollRecord(report);

} catch (error) {
console.error('Scheduled payroll failed:', error);
await sendPayrollFailureAlert(error);
}
});
}

async function getEmployeesFromDatabase() {
// Fetch employees from your database
// This is just an example - implement based on your system
return [
{
id: 'EMP001',
name: 'John Doe',
bankAccount: '1234567890',
accountName: 'John Doe',
bankName: 'BPI',
salary: 50000,
department: 'Engineering',
payPeriod: '2025-11'
},
// ... more employees
];
}

2. Compliance and Reporting

async function generateComplianceReport(batchId) {
const results = await getBatchResults(batchId);

const complianceData = {
batchId: batchId,
processedAt: new Date().toISOString(),
totalTransactions: results.payouts.length,
totalAmount: results.payouts.reduce((sum, p) => sum + p.amount, 0),
taxWithheld: calculateTax(results.payouts),
successfulTransactions: results.payouts.filter(p => p.status === 'COMPLETED'),
failedTransactions: results.payouts.filter(p => p.status === 'FAILED')
};

// Store for audit purposes
await storeComplianceRecord(complianceData);

return complianceData;
}

function calculateTax(payouts) {
// Implement tax calculation based on local regulations
return payouts.reduce((sum, p) => sum + (p.amount * 0.1), 0); // Example 10% tax
}

Error Handling and Best Practices

1. Idempotency

async function processPayrollWithIdempotency(employees, payrollDate) {
const idempotencyKey = `PAYROLL_${payrollDate}_${employees.length}`;

const response = await fetch('https://api.nextpay.world/v2/payouts/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
batchName: `Payroll_${payrollDate}`,
payouts: preparePayrollData(employees)
})
});

return await response.json();
}

2. Rate Limiting

class RateLimitedProcessor {
constructor(apiKey, requestsPerSecond = 10) {
this.apiKey = apiKey;
this.requestsPerSecond = requestsPerSecond;
this.lastRequestTime = 0;
}

async makeRequest(url, options) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
const minInterval = 1000 / this.requestsPerSecond;

if (timeSinceLastRequest < minInterval) {
await new Promise(resolve => setTimeout(resolve, minInterval - timeSinceLastRequest));
}

this.lastRequestTime = Date.now();

const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.apiKey}`
}
});

return response;
}
}

Testing Your Integration

1. Test with Small Batches

async function testPayrollProcessing() {
const testEmployees = [
{
id: 'TEST001',
name: 'Test Employee',
bankAccount: '1234567890',
accountName: 'Test Employee',
bankName: 'BPI',
salary: 1000, // Small amount for testing
department: 'Test',
payPeriod: '2025-11'
}
];

try {
const report = await processPayroll(testEmployees);
console.log('Test payroll successful:', report);
} catch (error) {
console.error('Test payroll failed:', error);
}
}

Conclusion

Implementing mass payouts with NextAPI provides:

  • ✅ Efficient batch processing for hundreds of payments
  • ✅ Real-time status monitoring and reporting
  • ✅ Automatic retry mechanisms for failed payments
  • ✅ Compliance and audit trail support
  • ✅ Scalable solution for growing payroll needs

Ready to process your first payroll? Start with our basic payout guide and scale up to batch processing.