A Paystack subscription that is not charging is usually caused by one of five things: the subscription is disabled or cancelled, the authorization code on the subscription is not reusable, the customer's card has expired, the plan amount was changed after the subscription was created, or the subscription is in arrears and Paystack has stopped retrying. Check the subscription status via the API, verify the authorization is reusable, confirm the card has not expired, and review the subscription's invoice history for failed charge attempts.
You set up a subscription plan on Paystack. A customer subscribes and the first payment goes through. Then recurring charges stop. The subscription appears to exist in your system, but no money comes in on the renewal date.
Or: a subscription has been working for months, and one month it just stops charging. No webhook, no error in your logs. The customer keeps using your service, and you do not notice until you check your revenue reports.
Both scenarios have the same root cause: something changed between the first charge and the next one, and Paystack can no longer process the recurring payment.
Work through this checklist in order. Each step eliminates a possible cause.
Step 1: Check the subscription status.
async function checkSubscriptionStatus(subscriptionCode) {
const response = await fetch(
`https://api.paystack.co/subscription/${subscriptionCode}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
const sub = data.data;
console.log('Status:', sub.status);
console.log('Next payment date:', sub.next_payment_date);
console.log('Authorization:', sub.authorization?.authorization_code);
console.log('Card last4:', sub.authorization?.last4);
console.log('Card exp:', sub.authorization?.exp_month + '/' + sub.authorization?.exp_year);
console.log('Reusable:', sub.authorization?.reusable);
console.log('Plan:', sub.plan?.name, sub.plan?.amount);
return sub;
}
Possible statuses:
If the status is anything other than "active," that is your answer. The subscription is not charging because it is not in a state where Paystack attempts charges.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Cards have expiration dates. When a customer's card expires, Paystack can no longer charge it. The subscription stays active, but every charge attempt fails.
function isCardExpired(expMonth, expYear) {
const now = new Date();
const currentMonth = now.getMonth() + 1; // JavaScript months are 0-indexed
const currentYear = now.getFullYear();
// Card expires at the end of the expiry month
if (expYear < currentYear) return true;
if (expYear === currentYear && expMonth < currentMonth) return true;
return false;
}
// Check a subscription's card
async function checkSubscriptionCard(subscriptionCode) {
const sub = await checkSubscriptionStatus(subscriptionCode);
const auth = sub.authorization;
if (!auth) {
return { valid: false, reason: 'No authorization found on subscription.' };
}
const expMonth = parseInt(auth.exp_month, 10);
const expYear = parseInt(auth.exp_year, 10);
// Paystack stores 2-digit years, convert to 4-digit
const fullYear = expYear < 100 ? 2000 + expYear : expYear;
if (isCardExpired(expMonth, fullYear)) {
return {
valid: false,
reason: `Card ending ${auth.last4} expired ${expMonth}/${fullYear}.`,
last4: auth.last4,
};
}
return { valid: true, last4: auth.last4, expires: `${expMonth}/${fullYear}` };
}
Proactive approach: before a card expires, notify the customer. Check all active subscriptions monthly, find cards expiring in the next 30 to 60 days, and send the customer an email asking them to update their payment method.
If you change a plan's amount after a customer has subscribed, the existing subscription does not automatically update. The subscription continues to reference the amount it was created with.
This creates confusion. You update your plan from NGN 5,000 to NGN 7,000 and expect existing subscribers to be charged NGN 7,000. But they are still charged NGN 5,000 (or the charge fails because of an internal mismatch, depending on how Paystack handles the discrepancy).
The correct approach for price changes:
async function migrateSubscription(subscriptionCode, newPlanCode, customerEmail) {
// Step 1: Get the current subscription to extract the authorization
const currentSub = await checkSubscriptionStatus(subscriptionCode);
const authCode = currentSub.authorization?.authorization_code;
if (!authCode) {
throw new Error('No authorization code on current subscription.');
}
// Step 2: Cancel the current subscription
// Use the email_token from the subscription
await fetch('https://api.paystack.co/subscription/disable', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: subscriptionCode,
token: currentSub.email_token,
}),
});
// Step 3: Create a new subscription on the new plan
const response = await fetch('https://api.paystack.co/subscription', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: customerEmail,
plan: newPlanCode,
authorization: authCode,
}),
});
const data = await response.json();
return data;
}
When a subscription charge fails, Paystack retries it. The retry schedule varies, but after several failed attempts (typically over a period of days), Paystack stops retrying and moves the subscription to an "attention" status.
At this point, the subscription is effectively stuck. Paystack will not attempt any more charges. The subscription is not cancelled (the customer might still want it), but it is not active either.
To recover a subscription in arrears:
async function getSubscriptionInvoices(subscriptionCode) {
const response = await fetch(
`https://api.paystack.co/subscription/${subscriptionCode}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
const invoices = data.data.invoices || [];
const failedInvoices = invoices.filter(inv => inv.status !== 'success');
console.log(`Total invoices: ${invoices.length}`);
console.log(`Failed invoices: ${failedInvoices.length}`);
failedInvoices.forEach(inv => {
console.log(` Invoice ${inv.invoice_code}: ${inv.status} - ${inv.description || 'no description'}`);
});
return { invoices, failedInvoices };
}
The invoice history tells you exactly when charges started failing and what happened. If the first failure was on the same date the card expired, you know the cause. If the failure message says "insufficient funds," the card is valid but the customer's bank account does not have enough money.
Do not wait for customers to complain about their subscription. Set up proactive monitoring.
Webhooks to listen for:
subscription.create: Log new subscriptions.subscription.disable or subscription.not_renew: The subscription was cancelled. Update your system.charge.success: A recurring charge went through. Extend the customer's access.invoice.payment_failed: A subscription charge failed. Alert your team and notify the customer.// Webhook handler for subscription events
function handleSubscriptionWebhook(event) {
switch (event.event) {
case 'invoice.payment_failed': {
const { subscription, customer } = event.data;
// Notify the customer that their payment failed
sendPaymentFailedEmail(customer.email, {
planName: subscription.plan?.name,
amount: subscription.plan?.amount / 100,
nextRetry: subscription.next_payment_date,
});
// Alert your team
notifyTeam(`Subscription payment failed for ${customer.email}`);
break;
}
case 'subscription.disable':
case 'subscription.not_renew': {
const { subscription_code, customer } = event.data;
// Update your database
deactivateSubscription(subscription_code);
console.log(`Subscription ${subscription_code} disabled for ${customer.email}`);
break;
}
case 'charge.success': {
const { authorization, customer, plan } = event.data;
if (plan) {
// This is a subscription charge, not a one-time payment
extendCustomerAccess(customer.email, plan.interval);
}
break;
}
}
}
Build a dashboard that shows subscription health: total active subscriptions, subscriptions with upcoming card expirations, subscriptions in arrears, and recent charge failures. Review it weekly.
When a subscription fails because of a card issue, the customer needs a way to update their payment method. Paystack does not provide a built-in card update page, so you need to build one.
The approach:
// After the customer completes the card update charge
async function updateSubscriptionCard(subscriptionCode, newAuthorizationCode, customerEmail) {
// Get current subscription
const sub = await checkSubscriptionStatus(subscriptionCode);
// Disable old subscription
await fetch('https://api.paystack.co/subscription/disable', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: subscriptionCode,
token: sub.email_token,
}),
});
// Create new subscription with new card
const response = await fetch('https://api.paystack.co/subscription', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: customerEmail,
plan: sub.plan.plan_code,
authorization: newAuthorizationCode,
start_date: sub.next_payment_date, // Continue from where the old subscription left off
}),
});
return response.json();
}
Email the customer a clear message: "Your subscription payment failed because your card ending 4242 has expired. Please update your card to continue your subscription." Include a direct link to your card update page.
For related guides on Paystack subscriptions and recurring billing:
Subscription billing is one of the trickiest parts of payment integration. The McTaba Full-Stack Software and AI Engineering course covers building production subscription systems with proper monitoring and failure recovery.
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