Subscriptions API vs Manual Recurring Charges: Choosing Correctly
Use the Subscriptions API when your billing has fixed amounts and standard intervals (monthly, quarterly, annually). Use manual charge_authorization when amounts vary, schedules are irregular, or you need control over retry logic. Many production systems use both. The Subscriptions API reduces code but limits flexibility. Manual charges give you full control but require you to build scheduling, retries, and lifecycle management yourself.
The Two Paths to Recurring Billing
Paystack offers recurring billing through two distinct mechanisms. Understanding the tradeoffs between them is one of the most important architectural decisions you will make when building a billing system for African markets.
Path 1: The Subscriptions API
You create a Plan (price + interval), subscribe a Customer to it, and Paystack handles the rest. Paystack generates invoices, charges the customer's saved card on each billing date, retries failures, and sends webhook events at each step. You listen and react.
Path 2: Manual recurring charges (charge_authorization)
You store the customer's authorization code after their first payment, then call charge_authorization on your own schedule with your own amounts. You build the scheduling, the retry logic, and the lifecycle management. Paystack just executes each charge.
Both paths use the same underlying mechanism: authorization codes. The Subscriptions API calls charge_authorization internally. The difference is who manages the schedule and the logic.
This article is part of the subscriptions and recurring billing guide.
Feature-by-Feature Comparison
| Dimension | Subscriptions API | Manual charge_authorization |
|---|---|---|
| Scheduling | Paystack manages. Charges happen on the billing date automatically. | You manage. You decide when to charge via cron jobs, task queues, or application logic. |
| Amount flexibility | Fixed. The plan defines the amount. Cannot change per cycle. | Fully flexible. You set the amount on each call. |
| Interval options | Standard only: daily, weekly, monthly, quarterly, biannually, annually. | Any schedule you want. Every 10 days, twice a term, on specific calendar dates. |
| Retry logic | Paystack retries failed charges automatically. | You build retry logic yourself. |
| Proration | Not supported. You handle proration manually even with subscriptions. | You calculate and charge prorated amounts yourself. |
| Webhook events | Rich lifecycle events: subscription.create, invoice.create, invoice.payment_failed, subscription.disable. | Only charge.success and charge.failed. You build your own lifecycle events. |
| Customer self-service | email_token enables Paystack-hosted management page. | You build your own management UI. |
| Code complexity | Lower. Create plan, subscribe customer, handle webhooks. | Higher. Build scheduler, state machine, retry queue, dunning flow. |
When to Use the Subscriptions API
The Subscriptions API is the right choice when your billing matches its constraints. If all of these are true, use it:
- The charge amount is the same every billing cycle
- The billing interval is one of: daily, weekly, monthly, quarterly, biannually, annually
- You want Paystack to handle retry logic for failed charges
- You do not need per-cycle proration or mid-cycle plan changes
Code for the Subscriptions API path:
var axios = require('axios');
var headers = {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
};
// Step 1: Create a plan (do this once, store the plan_code)
async function createPlan() {
var response = await axios.post(
'https://api.paystack.co/plan',
{
name: 'Pro Monthly',
amount: 500000,
interval: 'monthly',
currency: 'NGN',
},
{ headers: headers }
);
return response.data.data.plan_code;
}
// Step 2: Subscribe customer during checkout
async function subscribeCustomer(email, planCode) {
var response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: email,
amount: 500000,
plan: planCode,
},
{ headers: headers }
);
return response.data.data.authorization_url;
}
// Step 3: Handle webhooks
function handleSubscriptionWebhook(event) {
if (event.event === 'subscription.create') {
activateUser(event.data.customer.email, event.data.subscription_code);
}
if (event.event === 'invoice.payment_failed') {
startDunning(event.data.customer.email);
}
if (event.event === 'subscription.disable') {
deactivateUser(event.data.customer.email);
}
}
Real-world examples that fit: SaaS products with fixed pricing tiers, news or media subscriptions, streaming services, and any product with a standard monthly or annual fee.
When to Use Manual charge_authorization
Use manual charges when your billing does not fit the Subscriptions API constraints. If any of these are true, go manual:
- The charge amount varies each cycle (usage-based billing, instalment plans with different amounts)
- The schedule does not match standard intervals (school term dates, biweekly, every 10 days)
- You need complex retry logic with specific timing and escalation rules
- You charge multiple amounts per cycle (base fee plus overages)
- You need to apply discounts, credits, or prorations per charge
Code for the manual charge path:
var axios = require('axios');
var headers = {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
};
// Step 1: Collect first payment and store authorization
async function collectFirstPayment(email, amount) {
var response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: email,
amount: amount,
callback_url: 'https://yoursite.com/payment-callback',
},
{ headers: headers }
);
return response.data.data.authorization_url;
}
// Step 2: Store authorization from webhook
function handleChargeSuccess(eventData) {
var auth = eventData.authorization;
if (auth.reusable) {
saveAuthorization(eventData.customer.email, auth);
}
}
// Step 3: Charge on your own schedule
async function chargeCustomer(email, amount, authCode, reference) {
var response = await axios.post(
'https://api.paystack.co/transaction/charge_authorization',
{
email: email,
amount: amount,
authorization_code: authCode,
reference: reference,
},
{ headers: headers }
);
return response.data.data;
}
// Step 4: Run charges on a schedule (e.g., cron job)
async function runMonthlyBilling() {
var customers = await db.query(
'SELECT u.email, pm.authorization_code, b.amount_due FROM users u JOIN payment_methods pm ON u.id = pm.user_id JOIN billing b ON u.id = b.user_id WHERE b.next_charge_date <= NOW() AND pm.is_default = true'
);
for (var i = 0; i < customers.rows.length; i++) {
var c = customers.rows[i];
var ref = 'BILL_' + c.email + '_' + Date.now();
var result = await chargeCustomer(c.email, c.amount_due, c.authorization_code, ref);
if (result.status === 'success') {
await markBillingPaid(c.email, ref);
} else {
await scheduleDunning(c.email, result.gateway_response);
}
}
}
Real-world examples: school fee instalments, loan repayment collection, API usage billing, cloud infrastructure billing, and any product where the amount or schedule varies per customer.
The Hybrid Approach
Many production systems use both. This is not a compromise; it is often the best architecture.
Pattern: Base subscription + overage charges
Use the Subscriptions API for the base plan (fixed monthly fee). At the end of the month, calculate any overage (API calls beyond the included limit, storage beyond the quota) and charge it via charge_authorization.
// The Subscriptions API handles the base plan automatically.
// This function handles overages at the end of the billing period.
async function chargeOverage(userId) {
var usage = await calculateMonthlyUsage(userId);
var included = await getIncludedQuota(userId);
if (usage <= included) {
console.log('No overage for user ' + userId);
return;
}
var overageUnits = usage - included;
var overageAmount = overageUnits * PRICE_PER_UNIT;
var paymentMethod = await getDefaultPaymentMethod(userId);
var user = await getUser(userId);
var ref = 'OVERAGE_' + userId + '_' + Date.now();
var result = await chargeCustomer(
user.email,
overageAmount,
paymentMethod.authorization_code,
ref
);
if (result.status === 'success') {
await recordOveragePayment(userId, overageUnits, overageAmount, ref);
} else {
await handleOverageFailure(userId, overageAmount, result.gateway_response);
}
}
Pattern: Subscription with prorated upgrades
Base plan on the Subscriptions API. When the customer upgrades mid-cycle, calculate the prorated difference and charge it via charge_authorization. Cancel the old subscription and create a new one on the upgraded plan starting at the next billing date.
Pattern: Trial period managed manually, then subscription
Collect card details during signup with a minimal charge. Manage the trial period in your application. When the trial ends, create a Paystack subscription using the stored authorization. See free trials with Paystack subscriptions.
What You Must Build for Manual Charges
Choosing manual charge_authorization means you are building a billing engine. Here is what that requires:
1. A charge scheduler
A cron job, task queue (Bull, BullMQ), or cloud scheduler (AWS EventBridge, Google Cloud Scheduler) that triggers charges at the right time. This needs to be reliable. A missed cron run means missed revenue.
2. A state machine
Track each customer's billing state: trialing, active, past_due, cancelled, expired. Transition between states based on charge results and customer actions. See building your own billing engine.
3. Retry logic
When a charge fails, schedule retries with increasing delays. Track how many retries have been attempted. Stop after a maximum number. See handling failed charges and dunning.
4. Dunning communication
Email or SMS the customer when charges fail. Escalate the urgency with each retry. See building a dunning email sequence.
5. Invoice generation
Generate invoices or receipts for each charge. Store them in your database for accounting and customer access.
6. Reconciliation
Compare your charge records with Paystack's transaction records periodically. Catch any discrepancies (charges that succeeded on Paystack but your database missed, or vice versa).
This is real engineering work. Budget weeks or months for it, not days. The Subscriptions API exists to save you from building all of this for simple billing scenarios.
Decision Framework
Use this flowchart to decide your approach:
- Is the charge amount the same every cycle? If no, use manual charges.
- Does the interval match Paystack's options (daily/weekly/monthly/quarterly/annually)? If no, use manual charges.
- Do you need proration on plan changes? If yes, you need manual charges for the prorated portion (can be hybrid).
- Do you charge overages or add-ons on top of a base fee? If yes, use hybrid (subscription for base, manual for extras).
- Is your team small and you want to minimize billing code? If yes, lean toward the Subscriptions API where possible.
- Do you need full control over retry timing and escalation? If yes, use manual charges.
Mapping common African business models:
- SaaS with fixed tiers: Subscriptions API
- SaaS with usage-based pricing: Manual charges or hybrid
- School fee instalments: Manual charges
- Loan repayment collection: Manual charges
- Gym membership: Subscriptions API (with pause logic in your app)
- Insurance premiums: Subscriptions API (fixed amount, standard interval)
- Rent collection: Subscriptions API (fixed amount, monthly)
- Marketplace vendor payouts: Neither. Use Paystack Transfers instead.
Migrating Between Approaches
You might start with the Subscriptions API and later realize you need manual charges (or vice versa). Here is how to migrate.
From Subscriptions API to manual charges:
- Fetch all active subscriptions and their associated authorization codes.
- Store the authorization codes in your billing system.
- Record each customer's current billing cycle end date.
- Disable the Paystack subscriptions at the end of their current billing period (not mid-cycle, to avoid cutting off paid time).
- Start your manual charge scheduler to pick up where the subscriptions left off.
From manual charges to Subscriptions API:
- Create Paystack plans matching your pricing.
- For each customer, create a subscription using their stored authorization code.
- Time the subscription start date to align with their next billing date.
- Stop your manual charge scheduler for migrated customers.
- Update your webhook handlers to process subscription events.
In both cases, the authorization code carries over. The customer does not need to re-enter card details. The migration is about which system manages the schedule, not about the payment method.
Key Takeaways
- ✓The Subscriptions API manages scheduling, renewals, and retries for you. You create a plan, subscribe a customer, and listen to webhooks.
- ✓Manual charge_authorization gives you full control over timing, amounts, and retry logic. You build the billing engine yourself.
- ✓Fixed-price SaaS products fit the Subscriptions API well. Usage-based billing, instalment plans, and irregular schedules need manual charges.
- ✓The Subscriptions API does not support variable amounts, custom intervals, or proration. These require manual charges.
- ✓Many production systems use a hybrid: the Subscriptions API for base plans and manual charges for overages, one-time add-ons, or prorated adjustments.
- ✓Manual charges require more code but give you more control. Budget for building scheduling, retry logic, state management, and dunning.
Frequently Asked Questions
- Can I use the Subscriptions API with variable amounts?
- No. The Subscriptions API charges the fixed amount defined in the plan on every billing cycle. If you need to charge different amounts, use charge_authorization directly. You can use a hybrid approach where the base plan is a subscription and variable overages are manual charges.
- Is manual charge_authorization harder to implement than the Subscriptions API?
- Yes. The Subscriptions API handles scheduling, retries, and lifecycle events for you. With manual charges, you build all of that yourself: a scheduler, retry logic, state management, dunning, and invoice generation. The Subscriptions API might take a day to integrate. A full manual billing engine can take weeks.
- Can I switch a customer from a subscription to manual charges without them re-entering their card?
- Yes. The authorization code is independent of the subscription. Disable the subscription, store the authorization code from the subscription record, and start charging with charge_authorization. The customer does not need to do anything.
- Does the Subscriptions API support custom intervals like every 10 days?
- No. Paystack supports daily, weekly, monthly, quarterly, biannually, and annually. For custom intervals, you must use manual charge_authorization with your own scheduler.
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