Bonaventure OgetoBy Bonaventure Ogeto|

Paystack Subscription Not Charging

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.

Symptoms

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.

The Debug Checklist

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:

  • "active" means Paystack will attempt to charge on the next payment date. If it is active but not charging, the issue is with the payment method (Steps 2 to 4).
  • "non-renewing" means the subscription will not renew. It was either cancelled by the customer using the cancellation link, or your code disabled it.
  • "cancelled" means the subscription has been terminated. No further charges.
  • "attention" means Paystack tried to charge and failed too many times. Manual intervention is needed.

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.

Cause: Authorization Not Reusable

Every subscription is tied to an authorization code. This code represents the customer's card and their permission for you to charge it. For recurring billing to work, the authorization must be reusable.

Check the reusable flag on the authorization:

// From the subscription response
const sub = await checkSubscriptionStatus('SUB_abc123');

if (!sub.authorization?.reusable) {
  console.log('Authorization is NOT reusable. This is why the subscription is not charging.');
  console.log('Card type:', sub.authorization?.card_type);
  console.log('Bank:', sub.authorization?.bank);
}

If reusable is false, Paystack cannot charge this card again. The first payment went through because it was a direct charge (the customer entered their card details). But the authorization from that charge does not allow recurring charges.

This happens with:

  • Prepaid cards that do not support recurring billing.
  • Some Nigerian bank debit cards with restrictions on recurring charges.
  • Virtual cards from some providers.
  • Test cards that are not flagged as reusable.

The fix: ask the customer to update their payment method with a card that supports recurring billing. See Paystack Authorization Code Not Reusable for the full guide.

Cause: Card Expired

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.

Cause: Plan Amount Changed After Subscription Created

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:

  1. Create a new plan with the new amount.
  2. For existing subscribers, cancel their current subscription.
  3. Create a new subscription on the new plan using their existing authorization code.
  4. Notify the customer about the price change before doing any of this.
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;
}

Cause: Subscription in Arrears

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:

  1. Check the failed invoices to understand why the charges failed.
  2. If the card expired or was declined, ask the customer to update their payment method.
  3. Once the payment method is updated, charge the overdue amount using the new authorization.
  4. Re-enable the subscription or create a new one.
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.

Monitoring Subscription Health

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.

Customer Card Update Flow

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:

  1. Charge the customer a small amount (NGN 50 or the minimum) using the standard Paystack checkout.
  2. When the charge succeeds, you get a new authorization code from the new card.
  3. Update the subscription to use the new authorization.
  4. Refund the small charge if appropriate.
// 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.

Further Reading

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.

Key Takeaways

  • Check the subscription status first. If it is "non-renewing", "cancelled", or "attention", Paystack will not attempt to charge it. Only "active" subscriptions are charged.
  • The authorization code tied to the subscription must have reusable set to true. If the card does not support recurring billing, the authorization is not reusable, and all future charges will fail.
  • Expired cards cause silent subscription failures. Paystack cannot charge an expired card. The subscription stays active, but every charge attempt fails. Monitor for charge.failed events.
  • Changing a plan's amount does not update existing subscriptions on that plan. If you change a plan from NGN 5,000 to NGN 7,000, subscriptions created before the change still try to charge NGN 5,000. Create a new plan and migrate subscribers.
  • Paystack retries failed subscription charges, but it stops after a certain number of attempts. After that, the subscription moves to "attention" status and will not charge again unless you resolve the issue.
  • Set up webhooks for subscription.disable, charge.failed, and invoice.payment_failed to detect subscription problems as they happen, not weeks later when a customer complains.

Frequently Asked Questions

How do I know if a subscription is charging or not?
Check the subscription status via GET /subscription/{code}. If the status is "active" and the next_payment_date is in the future, Paystack will attempt to charge. Listen for charge.success and invoice.payment_failed webhooks to know the outcome of each charge attempt.
Can I manually trigger a subscription charge?
Not directly. Paystack manages the subscription charge schedule. If you need to charge a customer immediately, use the Charge Authorization endpoint (POST /transaction/charge_authorization) with their authorization code and the subscription amount. This is a one-time charge, not a subscription charge.
What happens when Paystack retries a failed subscription charge?
Paystack retries failed charges according to its internal schedule, typically a few times over several days. Each retry triggers an invoice.payment_failed webhook if it fails. After the final retry fails, the subscription moves to an "attention" status and Paystack stops retrying.
Can I change the card on an existing subscription?
Not directly via a simple API call. The process is: collect a new card from the customer (small charge via checkout), get the new authorization code, disable the old subscription, and create a new subscription with the new authorization code and the same plan. Set the start_date to maintain continuity.
Why did the first payment succeed but the subscription does not charge?
The first payment is a direct charge where the customer enters their card details. This always works if the card has funds. Recurring charges use the authorization code from that first charge. If the card does not support recurring billing (reusable is false), the first charge succeeds but all subsequent automatic charges fail.

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