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.
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:
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.
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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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.
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:
Paystack imposes limits on batch size and processing rate. The exact limits can change, so check the current documentation. General guidelines:
// 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;
}
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.
Before running bulk charges in production:
For webhook handling at scale, see the complete webhook engineering guide.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course