
Comprehensive Paystack Payment Guide
Explore general checkouts, split payouts, and webhook validation best practices.
Read Master Guide →
Building a successful Software-as-a-Service (SaaS) or recurring membership platform depends heavily on friction-free, automated billing systems. Forcing users to manually re-enter credit card details, initiate monthly bank transfers, or undergo 3D Secure OTP verification every 30 days introduces massive churn. Modern digital applications overcome this friction by leveraging payment tokenization, PCI-compliant authorization storage, and automated recurring billing engines.
Paystack offers a developer-friendly subscription ecosystem tailored for businesses across Africa and global markets. However, implementing a robust recurring revenue engine requires engineers to carefully evaluate subscription models, manage card token lifecycles, respond asynchronously to lifecycle webhooks, and design resilient dunning mechanisms. In this technical deep-dive, the Fort Engineering team breaks down the architectural patterns needed to build a enterprise-grade SaaS billing pipeline with Node.js and Paystack.
Architects must select the correct recurring payment paradigm based on their business model. Paystack supports two distinct approaches for collecting automated recurring payments:
In the Paystack-Managed approach, you define subscription plans (specifying amounts, billing intervals, and currency) on Paystack. When a user checks out, you attach the target plan_code to the checkout transaction initialization. Paystack automatically schedules and executes subsequent renewals on the due date without requiring custom cron jobs on your server. Paystack handles retry schedules for failed cards, renewal emails, and invoice generation, broadcasting state changes to your backend via webhooks.
If your application requires dynamic billing amounts, usage-based metering (e.g., charging per API call, seat count, or cloud bandwidth), or variable renewal intervals, Paystack-Managed plans may be too rigid. Instead, you can utilize card tokenization. Upon completing an initial successful payment, Paystack issues a reusable authorization_code. Your backend stores this token securely in your database and triggers programmatic charges on demand via the /transaction/charge_authorization API whenever your internal scheduler dictates.
When implementing Paystack-Managed subscriptions, plans can be created dynamically through the Paystack API or statically via the Paystack Merchant Dashboard. Programmatic creation allows engineering teams to keep subscription tiers synchronized across localized application databases and external billing portals.
Supported billing intervals include hourly, daily, weekly, monthly, quarterly, biannually, and annually. Monetary values must always be specified in the smallest currency subunit (e.g., ₦15,000.00 is submitted as 1500000 kobo).
Below is a production-ready Node.js service method for creating and retrieving subscription plans:
const axios = require('axios');
/**
* Programmatically creates a new SaaS Subscription Plan on Paystack
* @param {string} planName - Public name of the subscription plan
* @param {number} amountInNGN - Plan price in standard NGN units
* @param {string} interval - Billing frequency ('weekly', 'monthly', 'annually')
* @returns {Promise<Object>} Returns plan metadata including plan_code
*/
async function createSaaSPlan(planName, amountInNGN, interval = 'monthly') {
try {
const payload = {
name: planName,
interval: interval,
amount: Math.round(amountInNGN * 100), // Convert NGN to kobo
currency: 'NGN',
description: `Automated recurring plan for ${planName}`,
send_invoices: true, // Paystack automatically emails receipts to customers
send_sms: false
};
const response = await axios.post('https://api.paystack.co/plan', payload, {
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
});
if (response.data && response.data.status) {
console.log(`Plan Created: ${response.data.data.plan_code}`);
return response.data.data;
} else {
throw new Error(response.data.message || 'Plan creation failed.');
}
} catch (error) {
console.error('Paystack Plan Error:', error.response ? error.response.data : error.message);
throw error;
}
}
To enroll a customer into a recurring plan, pass the generated plan_code during transaction initialization. The user completes an initial 2-Factor Authentication (OTP/3DS) payment. Once authorized, Paystack locks the payment method to the plan and issues a reusable authorization object.
async function initializeSubscriptionCheckout(userEmail, planCode, userId) {
try {
const transactionRef = `SUB_INIT_${userId}_${Date.now()}`;
const payload = {
email: userEmail,
amount: 0, // Amount is inherited directly from the attached plan
plan: planCode,
reference: transactionRef,
callback_url: "https://yourdomain.com/dashboard/billing/confirm",
metadata: {
user_id: userId,
subscription_flow: true
}
};
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
payload,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json'
}
}
);
return response.data.data.authorization_url; // Redirect user to complete payment
} catch (error) {
console.error('Subscription Initialization Error:', error.response ? error.response.data : error);
throw new Error('Failed to start subscription process.');
}
}
For custom usage-based billing, extract the authorization_code from an initial successful payment webhook and save it in your user table. When a billing milestone occurs, invoke the charge authorization endpoint directly from your background job runner:
async function chargeSavedToken(authorizationCode, userEmail, amountInNGN) {
try {
const payload = {
authorization_code: authorizationCode,
email: userEmail,
amount: Math.round(amountInNGN * 100) // Convert to kobo
};
const response = await axios.post(
'https://api.paystack.co/transaction/charge_authorization',
payload,
{
headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` }
}
);
return response.data.data;
} catch (error) {
console.error('Token Charge Failed:', error.response ? error.response.data : error);
throw error;
}
}
Because subscription renewals execute asynchronously on Paystack's servers, your backend must maintain an event-driven webhook processing pipeline to manage user entitlement states. Never update database subscription states without validating the x-paystack-signature cryptographic header using HMAC-SHA512.
Below is an Express.js router handling core subscription lifecycle events:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/api/webhooks/paystack-subscriptions', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Verify HMAC Signature
const paystackSignature = req.headers['x-paystack-signature'];
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(req.body)
.digest('hex');
if (!paystackSignature || !crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(paystackSignature))) {
return res.status(401).send('Unauthorized webhook signature');
}
const event = JSON.parse(req.body.toString());
const data = event.data;
try {
switch (event.event) {
case 'subscription.create':
// Fired when a subscription is successfully created
await handleSubscriptionCreated(data);
break;
case 'charge.success':
// Fired on both initial payment and automatic recurring renewals
if (data.plan && data.plan.plan_code) {
await handleRenewalSuccess(data);
}
break;
case 'invoice.payment_failed':
// Fired when a recurring renewal attempt fails
await handleRenewalFailure(data);
break;
case 'subscription.disable':
// Fired when a subscription is explicitly cancelled or revoked
await handleSubscriptionCancelled(data);
break;
default:
console.log(`Unhandled event type: ${event.event}`);
}
// Always acknowledge receipt promptly with a 200 OK status
res.status(200).send('Event processed successfully');
} catch (error) {
console.error('Subscription Webhook Logic Error:', error);
res.status(500).send('Internal Server Error');
}
});
async function handleSubscriptionCreated(data) {
const { subscription_code, email_token, customer, plan, authorization } = data;
// Persist card tokenization details and update user entitlement state
await db.subscriptions.upsert({
where: { subscriptionCode: subscription_code },
update: {
status: 'ACTIVE',
nextPaymentDate: new Date(data.next_payment_date)
},
create: {
userEmail: customer.email,
subscriptionCode: subscription_code,
emailToken: email_token, // Token required to enable user-initiated cancellation
planCode: plan.plan_code,
authorizationCode: authorization.authorization_code,
cardLast4: authorization.last4,
cardBrand: authorization.card_type,
status: 'ACTIVE',
nextPaymentDate: new Date(data.next_payment_date)
}
});
}
async function handleRenewalSuccess(data) {
const subscriptionCode = data.subscription.subscription_code;
// Extend user access privileges
await db.subscriptions.update({
where: { subscriptionCode: subscriptionCode },
data: {
status: 'ACTIVE',
lastSuccessfulRenewal: new Date(),
nextPaymentDate: new Date(data.subscription.next_payment_date)
}
});
}
Recurring payments can fail for several reasons: expired credit cards, temporary bank account blocks, or insufficient funds on the renewal date. A naive application might instantly revoke user access upon the first failed attempt, frustrating users and increasing accidental churn. Professional SaaS platforms employ a structured Dunning Process to recover failed payments smoothly.
Paystack includes an automated smart retry engine that re-attempts failed cards over four sequential retries across several days. To align your application with this schedule, structure your dunning state machine as follows:
invoice.payment_failed event, flag the user's account as PAST_DUE. Maintain application features without disruption while notifying the user via email or in-app banners.subscription.disable event, transition the account status to EXPIRED or CANCELED, revoking premium feature access until the subscription is manually re-activated.Implementing this multi-stage recovery framework dramatically reduces involuntary churn while maintaining a smooth user experience.