Metered billing on Paystack uses a hybrid approach: the Subscriptions API charges the fixed base fee automatically, and charge_authorization handles overage charges at the end of the billing period. Track usage throughout the period, calculate overages when the period ends, and charge the difference. Two charges per period, one managed and one manual.
Metered billing is a hybrid between fixed subscriptions and pure usage-based billing. The customer pays a predictable base fee that includes a quota. Usage beyond the quota incurs overage charges.
Example: A messaging platform charges 3,000 NGN/month for the Pro plan. That includes 5,000 messages. Each message beyond 5,000 costs 1 NGN. A customer who sends 7,200 messages pays 3,000 NGN (base) + 2,200 NGN (overage) = 5,200 NGN.
This model works well because it gives customers a predictable base cost while scaling with heavy usage. It is common in API platforms, communication tools, cloud services, and data processing products.
On Paystack, implement this with two charges per period:
This article is part of the subscriptions and recurring billing guide.
async function trackAndCheckQuota(userId, eventType, quantity) {
// Record the usage event
await db.query(
'INSERT INTO usage_events (user_id, event_type, quantity, recorded_at) VALUES ($1, $2, $3, NOW())',
[userId, eventType, quantity]
);
// Check current usage against quota
var billing = await getCurrentBilling(userId);
var periodStart = billing.current_period_start;
var totalUsage = await db.query(
'SELECT COALESCE(SUM(quantity), 0) as total FROM usage_events WHERE user_id = $1 AND event_type = $2 AND recorded_at >= $3',
[userId, eventType, periodStart]
);
var used = parseFloat(totalUsage.rows[0].total);
var quota = await getIncludedQuota(billing.plan_id, eventType);
var percentUsed = Math.round((used / quota) * 100);
// Send alerts at thresholds
if (percentUsed >= 100 && !await alertSent(userId, eventType, 100)) {
sendQuotaAlert(userId, eventType, used, quota, 'exceeded');
markAlertSent(userId, eventType, 100);
} else if (percentUsed >= 80 && !await alertSent(userId, eventType, 80)) {
sendQuotaAlert(userId, eventType, used, quota, 'approaching');
markAlertSent(userId, eventType, 80);
}
return { used: used, quota: quota, percentUsed: percentUsed };
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
async function processOverageCharges() {
// Find accounts whose billing period just ended
var accounts = await db.query(
'SELECT ba.id, ba.user_id, ba.plan_id, ba.payment_method_id, ba.current_period_start, ba.current_period_end, u.email FROM billing_accounts ba JOIN users u ON ba.user_id = u.id WHERE ba.status = $1 AND ba.current_period_end <= NOW() AND ba.billing_type = $2',
['active', 'metered']
);
for (var i = 0; i < accounts.rows.length; i++) {
var account = accounts.rows[i];
// Get all usage types with quotas for this plan
var pricingRules = await db.query(
'SELECT event_type, included_units, price_per_unit FROM usage_pricing WHERE plan_id = $1',
[account.plan_id]
);
var totalOverage = 0;
var overageItems = [];
for (var j = 0; j < pricingRules.rows.length; j++) {
var rule = pricingRules.rows[j];
var usage = await db.query(
'SELECT COALESCE(SUM(quantity), 0) as total FROM usage_events WHERE user_id = $1 AND event_type = $2 AND recorded_at >= $3 AND recorded_at < $4',
[account.user_id, rule.event_type, account.current_period_start, account.current_period_end]
);
var usedUnits = parseFloat(usage.rows[0].total);
var overageUnits = Math.max(0, usedUnits - rule.included_units);
if (overageUnits > 0) {
var overageAmount = Math.round(overageUnits * rule.price_per_unit);
totalOverage = totalOverage + overageAmount;
overageItems.push({
eventType: rule.event_type,
used: usedUnits,
included: rule.included_units,
overage: overageUnits,
amount: overageAmount,
});
}
}
if (totalOverage === 0) {
console.log('No overages for user ' + account.user_id);
continue;
}
// Charge the overage
var authCode = await getAuthorizationCode(account.payment_method_id);
var reference = 'OVERAGE_' + account.user_id + '_' + account.current_period_start.toISOString().slice(0, 7);
var result = await chargeAuthorization(account.email, totalOverage, authCode, reference);
if (result.status === 'success') {
await createOverageInvoice(account.user_id, overageItems, totalOverage, reference);
console.log('Overage charged: ' + totalOverage + ' for user ' + account.user_id);
} else {
await handleOverageFailure(account.user_id, totalOverage, result.gateway_response);
}
}
}
Run this after the subscription renewal has processed. The base fee is charged by Paystack automatically. The overage charge runs in your billing job, typically within hours of the period ending.
Overage charges surprise customers. Reduce friction with clear communication.
Before the charge: Send a usage summary email when the billing period ends and before the overage charge hits. "Your usage this month: 7,200 messages. Your included quota: 5,000 messages. Overage: 2,200 messages at 1 NGN each = 2,200 NGN. This will be charged to your Visa ending in 4081."
After the charge: Send a detailed receipt with line items showing the overage breakdown.
During the period: Show real-time usage in the customer dashboard. Send alerts when they hit 80% and 100% of their quota.
// Usage dashboard API
router.get('/api/billing/metered-usage', async function(req, res) {
var userId = req.user.id;
var billing = await getCurrentBilling(userId);
var pricingRules = await db.query(
'SELECT event_type, included_units, price_per_unit FROM usage_pricing WHERE plan_id = $1',
[billing.plan_id]
);
var usageSummary = [];
for (var i = 0; i < pricingRules.rows.length; i++) {
var rule = pricingRules.rows[i];
var usage = await db.query(
'SELECT COALESCE(SUM(quantity), 0) as total FROM usage_events WHERE user_id = $1 AND event_type = $2 AND recorded_at >= $3',
[userId, rule.event_type, billing.current_period_start]
);
var used = parseFloat(usage.rows[0].total);
var overage = Math.max(0, used - rule.included_units);
usageSummary.push({
type: rule.event_type,
used: used,
included: rule.included_units,
overage: overage,
overageCost: Math.round(overage * rule.price_per_unit),
percentUsed: Math.round((used / rule.included_units) * 100),
});
}
res.json({
usage: usageSummary,
periodStart: billing.current_period_start,
periodEnd: billing.current_period_end,
});
});
If a customer consistently exceeds their quota, an upgrade to a higher plan with a larger quota might be cheaper than paying overages. Calculate and suggest this proactively.
async function checkUpgradeSuggestion(userId) {
var billing = await getCurrentBilling(userId);
var currentPlan = await getPlan(billing.plan_id);
var nextPlan = await getNextHigherPlan(currentPlan.tier);
if (!nextPlan) return null; // Already on the highest plan
// Check last 3 months of overages
var recentOverages = await db.query(
'SELECT SUM(amount) as total FROM invoices WHERE billing_account_id = $1 AND invoice_type = $2 AND created_at > NOW() - INTERVAL '3 months'',
[billing.id, 'overage']
);
var avgMonthlyOverage = Math.round(parseFloat(recentOverages.rows[0].total || 0) / 3);
var upgradeCostDifference = nextPlan.base_amount - currentPlan.base_amount;
if (avgMonthlyOverage > upgradeCostDifference) {
return {
suggest: true,
currentPlan: currentPlan.name,
suggestedPlan: nextPlan.name,
currentCost: currentPlan.base_amount + avgMonthlyOverage,
upgradedCost: nextPlan.base_amount,
savings: (currentPlan.base_amount + avgMonthlyOverage) - nextPlan.base_amount,
message: 'Based on your usage, upgrading to ' + nextPlan.name + ' would save you about ' + formatAmount((currentPlan.base_amount + avgMonthlyOverage) - nextPlan.base_amount) + ' per month.',
};
}
return null;
}
Display upgrade suggestions in the billing dashboard and in overage notification emails. This is a win-win: the customer saves money and you get more predictable revenue.
Overage charges can fail just like any recurring charge. The base subscription might succeed (Paystack handles that), but the overage charge (your charge_authorization call) might fail.
Options for handling this:
Choose the approach that matches your business. For low-value overages, carrying forward is simplest. For significant amounts, dedicated dunning is worth the engineering effort.
For the complete dunning implementation, see handling failed recurring charges.
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