Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.zuba.com/llms.txt

Use this file to discover all available pages before exploring further.

Batch payouts allow you to process multiple payments simultaneously, making it ideal for payroll, affiliate payments, or any scenario requiring multiple transfers. This guide covers best practices and implementation details.

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