Bonaventure OgetoBy Bonaventure Ogeto|

Paystack Bulk Charge: Charging Many Customers at Once

The Paystack Bulk Charge API lets you submit an array of charges (each with an authorization_code, amount, and reference) in a single POST to /bulkcharge. Paystack processes them asynchronously and you poll the batch status or listen for webhooks to track individual charge results. Use it for subscription renewals, loan collections, or any scenario where you charge many saved cards on a schedule.

When to Use Bulk Charge

Bulk charge is designed for scenarios where you need to charge many customers at the same time. Calling /transaction/charge_authorization in a loop for 500 customers works but is slow, hits rate limits, and requires you to manage concurrency yourself. Bulk charge handles this for you.

Common use cases:

  • Subscription renewals: On the 1st of the month, charge all active subscribers at once.
  • Loan repayments: Collect scheduled payments from all borrowers on the due date.
  • Savings deductions: Automated savings platforms that debit a fixed amount from each customer weekly or monthly.
  • Membership fees: Annual or monthly membership renewals for clubs, gyms, or associations.
  • Payroll deductions: Employer-deducted payments for insurance, savings schemes, or loan repayments.

If you are charging fewer than 10 customers, individual charge_authorization calls are simpler. Bulk charge adds value when the batch size makes individual calls impractical.

Submitting a Batch

Send a POST request to /bulkcharge with an array of charge objects:

async function submitBulkCharge(charges) {
  // charges is an array like:
  // [
  //   { authorization: 'AUTH_xxx', amount: 500000, reference: 'sub_001_jul2026' },
  //   { authorization: 'AUTH_yyy', amount: 500000, reference: 'sub_002_jul2026' },
  //   ...
  // ]

  var response = await fetch('https://api.paystack.co/bulkcharge', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(charges),
  });

  var data = await response.json();

  if (data.status) {
    // data.data.batch_code identifies this batch
    return {
      batchCode: data.data.batch_code,
      status: data.data.status,
    };
  }

  throw new Error('Bulk charge submission failed: ' + data.message);
}

// Build the charges array from your database
async function buildChargesForRenewal() {
  var subscribers = await db.subscriptions.findAll({
    where: { status: 'active', renewal_date: today() },
  });

  return subscribers.map(function(sub) {
    return {
      authorization: sub.authorization_code,
      amount: sub.plan_amount,
      reference: 'renewal_' + sub.id + '_' + currentMonth(),
    };
  });
}

// Submit
var charges = await buildChargesForRenewal();
var batch = await submitBulkCharge(charges);
console.log('Batch submitted: ' + batch.batchCode);

The API returns immediately. Paystack processes the charges in the background. The batch_code is your handle for tracking progress.

Tracking Batch Status

After submitting, you can poll the batch status and query individual charge results:

// Check overall batch status
async function checkBatchStatus(batchCode) {
  var response = await fetch(
    'https://api.paystack.co/bulkcharge/' + batchCode,
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  var data = await response.json();
  // data.data.status: 'active', 'paused', 'complete', 'failed'
  return data.data;
}

// Get individual charge results within a batch
async function getBatchCharges(batchCode) {
  var response = await fetch(
    'https://api.paystack.co/bulkcharge/' + batchCode + '/charges',
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  var data = await response.json();
  // data.data is an array of individual charge results
  return data.data;
}

// Poll until complete
async function waitForBatchCompletion(batchCode) {
  var maxAttempts = 60;
  var attempt = 0;

  while (attempt < maxAttempts) {
    var batch = await checkBatchStatus(batchCode);

    if (batch.status === 'complete') {
      return await getBatchCharges(batchCode);
    }

    if (batch.status === 'failed') {
      throw new Error('Batch failed');
    }

    // Wait 30 seconds before checking again
    await new Promise(function(resolve) { setTimeout(resolve, 30000); });
    attempt++;
  }

  throw new Error('Batch processing timed out');
}

You can also rely on webhooks instead of polling. Paystack sends charge.success and charge.failed events for each individual charge in the batch.

Handling Mixed Results

In any batch, some charges will succeed and some will fail. Your code must handle each outcome independently:

async function processResults(batchCode) {
  var charges = await getBatchCharges(batchCode);
  var succeeded = [];
  var failed = [];

  for (var i = 0; i < charges.length; i++) {
    var charge = charges[i];

    if (charge.status === 'success') {
      succeeded.push(charge);
      // Activate/renew the subscription
      await db.subscriptions.renew(charge.reference);
    } else {
      failed.push(charge);
      // Mark subscription as payment failed
      await db.subscriptions.markPaymentFailed(charge.reference, charge.gateway_response);
    }
  }

  console.log('Succeeded: ' + succeeded.length);
  console.log('Failed: ' + failed.length);

  // Handle failures: retry, notify, or downgrade
  for (var j = 0; j < failed.length; j++) {
    var failedCharge = failed[j];
    await scheduleRetry(failedCharge.reference);
    await notifyCustomerPaymentFailed(failedCharge.reference);
  }

  return { succeeded: succeeded, failed: failed };
}

Common failure reasons in bulk charge:

  • Insufficient funds: The most common. The customer does not have enough money.
  • Invalid authorization: The auth code has expired or the card was replaced.
  • Bank decline: The customer's bank declined the charge for various reasons.
  • Duplicate reference: You submitted the same reference twice in the batch or a reference that was already used.

Batch Size and Rate Limits

Paystack imposes limits on batch size and processing rate. The exact limits can change, so check the current documentation. General guidelines:

  • There is a maximum number of charges per batch (typically in the hundreds to low thousands).
  • If you need to charge more than the limit allows, split your charges into multiple batches and submit them sequentially.
  • Processing time scales with batch size. A batch of 50 charges completes faster than a batch of 500.
// Split large lists into batches
function chunkArray(array, chunkSize) {
  var chunks = [];
  for (var i = 0; i < array.length; i += chunkSize) {
    chunks.push(array.slice(i, i + chunkSize));
  }
  return chunks;
}

async function submitLargeBulkCharge(allCharges) {
  var batchSize = 500; // Check Paystack docs for current limit
  var chunks = chunkArray(allCharges, batchSize);
  var batches = [];

  for (var i = 0; i < chunks.length; i++) {
    var batch = await submitBulkCharge(chunks[i]);
    batches.push(batch);
    console.log('Submitted batch ' + (i + 1) + ' of ' + chunks.length);
  }

  return batches;
}

Designing References for Bulk Charges

Each charge in a bulk batch needs a unique reference. Good references for bulk charges include enough information to identify the customer, the billing period, and the charge type.

// Pattern: type_customerId_period
var reference = 'renewal_' + subscription.id + '_2026-07';
// Example: renewal_sub_142_2026-07

// Pattern: type_customerId_timestamp
var reference = 'loan_' + loan.id + '_' + Date.now();
// Example: loan_ln_089_1721472000000

Including the billing period in the reference prevents duplicate charges. If you accidentally submit the same batch twice, all references will already exist and Paystack will reject the duplicates. This is your safety net against double-charging customers.

For more on reference design, see transaction reference design patterns.

Production Checklist

Before running bulk charges in production:

  1. Test with a small batch first: Submit 2-3 test charges in test mode to verify your submission, polling, and result handling code works.
  2. Validate references: Ensure all references are unique within the batch and across previous batches for the same billing period.
  3. Have a retry strategy: Failed charges should be retried (with a delay) a limited number of times before you escalate to customer notification.
  4. Log everything: Log the batch code, individual results, and any errors. You will need this for debugging and for responding to customer inquiries.
  5. Monitor webhook delivery: Bulk charge generates many webhooks in a short time. Make sure your webhook endpoint can handle the load.
  6. Run at off-peak hours: If you have thousands of charges, submit them during off-peak hours to reduce the impact of any processing delays.

For webhook handling at scale, see the complete webhook engineering guide.

Stay Up to Date

Paystack updates bulk charge limits and features periodically. We keep these guides current.

Sign up for the McTaba newsletter to stay informed about Paystack API changes.

Key Takeaways

  • Bulk charge accepts an array of charge objects, each with an authorization_code, amount, and reference. You submit them all in one API call.
  • Processing is asynchronous. The API returns immediately with a batch ID. You poll the batch status or listen for webhooks to track results.
  • Each charge in the batch is processed independently. Some may succeed, some may fail. Your code must handle mixed results.
  • Bulk charge only works with saved authorization codes. You cannot use it for first-time card payments.
  • Include meaningful references in each charge so you can map results back to specific customers or orders.
  • Rate limits and batch size limits apply. Check Paystack documentation for current limits and plan your batches accordingly.

Frequently Asked Questions

Can I pause or cancel a bulk charge batch?
Paystack provides endpoints to pause and resume a bulk charge batch. You can pause processing if you discover an issue and resume once resolved. Check the Paystack documentation for the exact endpoints.
Does bulk charge support partial debits?
Bulk charge processes each charge as a standard charge_authorization call. Support for the at_least parameter in bulk charge may be limited. For partial debit support in batch scenarios, you may need to submit individual charge_authorization calls.
How long does bulk charge processing take?
Processing time depends on the batch size and Paystack server load. Small batches (under 100 charges) may complete in minutes. Large batches (thousands) can take significantly longer. Plan for the possibility that processing takes hours for very large batches.
What happens if I submit the same reference twice in a batch?
Paystack rejects duplicate references. If the same reference appears twice in a batch, one of them will fail. This is a safety feature that prevents double-charging.
Can I include metadata in bulk charge items?
Bulk charge items support a limited set of fields: authorization code, amount, and reference. For metadata, you would need to use individual charge_authorization calls instead, or track the additional data in your own database mapped to the reference.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon