Metered Billing and Overages on Paystack
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.
The Metered Billing Model
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:
- Base fee: Handled by the Subscriptions API. Charges automatically on the billing date.
- Overage charge: Calculated at the end of the period and charged via charge_authorization.
This article is part of the subscriptions and recurring billing guide.
Tracking Usage Against the Quota
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 };
}
Calculating and Charging Overages
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.
Communicating Overages to Customers
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,
});
});
Suggesting Plan Upgrades Instead of Overages
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.
Handling Overage Charge Failures
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:
- Add to next month's bill. Carry the overage amount forward and include it in the next billing period's overage calculation.
- Separate dunning for overages. Start a dunning sequence specifically for the overage charge, independent of the subscription status.
- Restrict overage-generating features. If the overage charge fails, cap the customer at their included quota for the next period until the overage is paid.
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.
Key Takeaways
- ✓Metered billing charges a base fee (fixed) plus overage charges (variable). Paystack handles the base fee via Subscriptions API. You handle overages via charge_authorization.
- ✓Track usage in your database. At the end of the billing period, compare usage to the included quota. Charge for any excess.
- ✓The overage charge happens separately from the subscription renewal. Customers may see two charges in one billing period.
- ✓Communicate the metered model clearly. Customers should understand their included quota, the overage rate, and how to monitor their usage.
- ✓Set up usage alerts at 80% and 100% of the included quota. Nobody wants a surprise overage charge.
- ✓Consider offering a plan upgrade as an alternative to overages. If a customer consistently exceeds their quota, upgrading to a higher plan may be cheaper.
Frequently Asked Questions
- Will customers see two charges on their card statement?
- Yes. The base subscription charge (via Subscriptions API) and the overage charge (via charge_authorization) are separate transactions. Communicate this clearly in your billing documentation so customers expect two charges.
- What if the base subscription charge succeeds but the overage charge fails?
- The subscription remains active (base fee was paid). The overage amount becomes outstanding. You can carry it forward to the next period, start a separate dunning sequence, or restrict overage-generating features until the overage is paid.
- Should I charge overages at the end of the period or in real-time?
- End-of-period is simpler and more common. Real-time overage charging (charging as the customer exceeds the quota) is complex and can result in many small charges. Most products bill overages once at the end of the billing period.
- How do I handle tiered overage pricing?
- Tiered pricing means the overage rate changes at different usage levels (first 1,000 extra at 1 NGN each, next 5,000 at 0.80 NGN each). Store tier boundaries in your usage_pricing table and calculate the charge by applying each tier's rate to the appropriate quantity range.
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