Why Use Batch Payouts
- Efficiency: Process hundreds of payouts with a single API call
- Cost-effective: Reduced API overhead and potential fee savings
- Atomic operations: All payouts succeed or fail together
- Better error handling: Centralized validation and error reporting
Single API Call for Multiple Payouts
The Zuba API accepts both single payout objects and arrays of payout objects at the same endpoint. Currency Fields:inputCurrency is the currency from your account you’ll be paying from, while currency is what the beneficiary will receive (automatic conversion if different):
// Batch payout data
const batchPayouts = [
{
clientRef: "SALARY-JAN-2024-001",
amount: "1500.00",
inputCurrency: "EUR",
currency: "EUR",
route: "sepa_inst",
beneficiary: { id: "emp-001-uuid" },
senderInfo: {
firstName: "Acme",
lastName: "Corp",
address: "123 Business Ave",
city: "Berlin",
postalCode: "10115",
country: "DE"
},
reference: "January 2024 salary - John Doe",
description: "Monthly salary payment"
},
{
clientRef: "SALARY-JAN-2024-002",
amount: "2000.00",
inputCurrency: "EUR",
currency: "EUR",
route: "sepa_inst",
beneficiary: { id: "emp-002-uuid" },
senderInfo: {
firstName: "Acme",
lastName: "Corp",
address: "123 Business Ave",
city: "Berlin",
postalCode: "10115",
country: "DE"
},
reference: "January 2024 salary - Jane Smith",
description: "Monthly salary payment"
},
{
clientRef: "SALARY-JAN-2024-003",
amount: "1200.00",
inputCurrency: "EUR",
currency: "EUR",
route: "sepa_credit",
beneficiary: { id: "emp-003-uuid" },
senderInfo: {
firstName: "Acme",
lastName: "Corp",
address: "123 Business Ave",
city: "Berlin",
postalCode: "10115",
country: "DE"
},
reference: "January 2024 salary - Bob Wilson",
description: "Monthly salary payment"
}
];
const response = await fetch('https://api-test.zuba.com/v1/payouts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
body: JSON.stringify(batchPayouts)
});
const payouts = await response.json();
console.log(`Created ${payouts.length} payouts`);
Preparing Beneficiaries
Before creating batch payouts, ensure all beneficiaries exist. Here’s how to bulk create them:async function createBeneficiaries(employeeData) {
const beneficiaries = [];
for (const employee of employeeData) {
const beneficiaryData = {
name: employee.name,
email: employee.email,
country: "DE",
address: "Sonnenstrasse 15 80331 Muenchen Germany",
accounts: [{
type: "iban",
currency: "EUR",
data: {
iban: employee.iban,
bic: employee.bic
}
}]
};
try {
const response = await fetch('https://api-test.zuba.com/v1/beneficiaries', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
body: JSON.stringify(beneficiaryData)
});
const beneficiary = await response.json();
beneficiaries.push({
...employee,
beneficiaryId: beneficiary.id
});
} catch (error) {
console.error(`Failed to create beneficiary for ${employee.name}:`, error);
}
}
return beneficiaries;
}
CSV Import Example
Process payroll data from a CSV file:import csv from 'csv-parser';
import fs from 'fs';
async function processBatchPayoutsFromCSV(filePath) {
const payouts = [];
const beneficiaryMap = new Map();
return new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
// Validate row data
if (!row.name || !row.amount || !row.iban) {
console.warn('Skipping invalid row:', row);
return;
}
payouts.push({
name: row.name,
email: row.email,
amount: parseFloat(row.amount),
currency: row.currency || 'EUR',
iban: row.iban,
bic: row.bic,
reference: row.reference,
description: row.description
});
})
.on('end', async () => {
try {
// Create beneficiaries first
const beneficiaries = await createBeneficiaries(payouts);
// Create batch payouts
const payoutRequests = beneficiaries.map((emp, index) => ({
clientRef: `BATCH-${Date.now()}-${index}`,
amount: String(emp.amount),
inputCurrency: emp.inputCurrency || "EUR",
currency: emp.currency,
route: "sepa_inst",
beneficiary: { id: emp.beneficiaryId },
reference: emp.reference,
description: emp.description
}));
const response = await fetch('https://api-test.zuba.com/v1/payouts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
body: JSON.stringify(payoutRequests)
});
const results = await response.json();
resolve(results);
} catch (error) {
reject(error);
}
})
.on('error', reject);
});
}
Monitoring Batch Progress
Track the status of all payouts in a batch:async function monitorBatchPayouts(payoutIds) {
const statusUpdates = [];
for (const payoutId of payoutIds) {
try {
const response = await fetch(`https://api-test.zuba.com/v1/payouts/${payoutId}`, {
headers: {
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
}
});
const payout = await response.json();
statusUpdates.push({
id: payout.id,
reference: payout.reference,
status: payout.status,
amount: payout.amount,
beneficiaryName: payout.beneficiary.name
});
} catch (error) {
console.error(`Error checking payout ${payoutId}:`, error);
}
}
return statusUpdates;
}
Error Handling in Batches
Handle partial failures gracefully:async function createBatchWithErrorHandling(payoutRequests) {
try {
const response = await fetch('https://api-test.zuba.com/v1/payouts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
},
body: JSON.stringify(payoutRequests)
});
if (!response.ok) {
const error = await response.json();
// Handle validation errors for specific payouts
if (error.details && Array.isArray(error.details)) {
error.details.forEach((detail, index) => {
console.error(`Payout ${index + 1} failed:`, detail.message);
});
}
throw new Error(error.message);
}
const payouts = await response.json();
// Log successful payouts
payouts.forEach((payout, index) => {
console.log(`Payout ${index + 1}: ${payout.id} - ${payout.status}`);
});
return payouts;
} catch (error) {
console.error('Batch payout failed:', error.message);
throw error;
}
}
Best Practices
1. Batch Size Limits
Keep batch sizes reasonable to avoid timeouts:const BATCH_SIZE = 100;
async function processBatchesInChunks(payouts) {
const results = [];
for (let i = 0; i < payouts.length; i += BATCH_SIZE) {
const batch = payouts.slice(i, i + BATCH_SIZE);
try {
const batchResults = await createBatchWithErrorHandling(batch);
results.push(...batchResults);
// Add delay between batches to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`Batch ${i / BATCH_SIZE + 1} failed:`, error);
}
}
return results;
}
2. Validation Before Submission
Validate all payout data before submission:function validateBatchPayouts(payouts) {
const errors = [];
payouts.forEach((payout, index) => {
if (!payout.clientRef) {
errors.push(`Payout ${index + 1}: Missing clientRef`);
}
if (!payout.beneficiary?.id) {
errors.push(`Payout ${index + 1}: Missing beneficiary.id`);
}
const amount = parseFloat(payout.amount);
if (!payout.amount || isNaN(amount) || amount <= 0) {
errors.push(`Payout ${index + 1}: Invalid amount`);
}
if (!['EUR', 'USD', 'GBP'].includes(payout.currency)) {
errors.push(`Payout ${index + 1}: Unsupported currency`);
}
if (!['sepa_inst', 'sepa_credit', 'bank_transfer'].includes(payout.route)) {
errors.push(`Payout ${index + 1}: Invalid route`);
}
});
return errors;
}
// Usage
const validationErrors = validateBatchPayouts(batchPayouts);
if (validationErrors.length > 0) {
console.error('Validation errors:', validationErrors);
return;
}
3. Progress Reporting
Implement progress tracking for large batches:async function createBatchWithProgress(payouts, onProgress) {
const results = [];
const total = payouts.length;
for (let i = 0; i < total; i += BATCH_SIZE) {
const batch = payouts.slice(i, i + BATCH_SIZE);
const batchResults = await createBatchWithErrorHandling(batch);
results.push(...batchResults);
// Report progress
const processed = Math.min(i + BATCH_SIZE, total);
onProgress(processed, total, (processed / total) * 100);
}
return results;
}
// Usage
const payouts = await createBatchWithProgress(batchData, (current, total, percent) => {
console.log(`Progress: ${current}/${total} (${percent.toFixed(1)}%)`);
});
Complete Batch Payout Example
Here’s a complete implementation:class BatchPayoutProcessor {
constructor(apiToken, baseURL = 'https://api-test.zuba.com/v1') {
this.apiToken = apiToken;
this.baseURL = baseURL;
this.headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
};
}
async processPayroll(employeeData) {
try {
// 1. Create beneficiaries
console.log('Creating beneficiaries...');
const beneficiaries = await this.createBeneficiaries(employeeData);
// 2. Prepare payout requests
const payoutRequests = beneficiaries.map((emp, index) => ({
clientRef: `PAYROLL-${new Date().getMonth() + 1}-${emp.employeeId}`,
amount: String(emp.salary),
inputCurrency: 'EUR',
currency: 'EUR',
route: 'sepa_inst',
beneficiary: { id: emp.beneficiaryId },
reference: `Monthly salary - ${emp.name}`,
description: `Payroll payment for ${emp.name}`
}));
// 3. Validate payouts
const errors = this.validatePayouts(payoutRequests);
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
// 4. Create batch payouts
console.log(`Creating ${payoutRequests.length} payouts...`);
const payouts = await this.createBatch(payoutRequests);
// 5. Return summary
return {
total: payouts.length,
successful: payouts.filter(p => p.status === 'created').length,
totalAmount: payouts.reduce((sum, p) => sum + p.amount, 0),
payouts: payouts
};
} catch (error) {
console.error('Payroll processing failed:', error);
throw error;
}
}
async createBatch(payouts) {
const response = await fetch(`${this.baseURL}/payouts`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(payouts)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return await response.json();
}
// ... other methods
}
// Usage
const processor = new BatchPayoutProcessor(process.env.ZUBA_API_TOKEN);
const results = await processor.processPayroll(employeeData);
console.log(`Processed ${results.successful}/${results.total} payouts`);
Next Steps
- Set up webhooks to receive real-time status updates for your batch payouts
- Learn about error handling strategies for production batch processing
- Explore the API reference for advanced payout options