McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Cancelling and Pausing Paystack Subscriptions
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Cancelling and Pausing Paystack Subscriptions

Cancel a Paystack subscription by calling POST /subscription/disable with the subscription code and email token. Paystack stops all future charges immediately. There is no native pause feature. To pause, cancel the subscription and store the customer's authorization code. When they want to resume, create a new subscription using the stored authorization. Track cancellation type (immediate vs end-of-period) in your application to handle access correctly.

In this article

  1. 1. Cancelling a Subscription
  2. 2. Immediate vs End-of-Period Cancellation
  3. 3. Implementing Pause (Without a Native Feature)
  4. 4. Setting Pause Limits
  5. 5. The Reactivation Flow
  6. 6. Tracking Cancellation Reasons

Cancelling a Subscription

Paystack provides one mechanism for stopping a subscription: the disable endpoint. It takes two parameters: the subscription code and the email token.

var axios = require('axios');

async function cancelSubscription(subscriptionCode, emailToken) {
  var response = await axios.post(
    'https://api.paystack.co/subscription/disable',
    {
      code: subscriptionCode,
      token: emailToken,
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data;
}

When you disable a subscription:

  • Paystack stops all future automatic charges.
  • Paystack sends a subscription.disable webhook event.
  • The subscription status changes to "cancelled" or "non-renewing" depending on timing.
  • The authorization code is not affected. You can still charge the customer manually or create a new subscription later.

You can also re-enable a disabled subscription:

async function reEnableSubscription(subscriptionCode, emailToken) {
  var response = await axios.post(
    'https://api.paystack.co/subscription/enable',
    {
      code: subscriptionCode,
      token: emailToken,
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data;
}

This article is part of the subscriptions and recurring billing guide.

Immediate vs End-of-Period Cancellation

Paystack's disable endpoint is immediate: it stops future charges right now. But the business question is different: should the customer lose access immediately or keep it until the end of their paid period?

Most subscription businesses use end-of-period cancellation. The customer paid for a month (or year). They should get what they paid for, even if they cancel on day 2.

Paystack does not manage this for you. You implement it in your application:

async function requestCancellation(userId) {
  var subscription = await getActiveSubscription(userId);

  if (!subscription) {
    throw new Error('No active subscription to cancel');
  }

  // Disable on Paystack (stops future charges)
  await cancelSubscription(
    subscription.paystack_subscription_code,
    subscription.paystack_email_token
  );

  // In your database: mark as cancelling but keep access until period end
  await db.query(
    'UPDATE billing_accounts SET status = $1, cancelled_at = NOW(), access_until = $2, cancellation_reason = $3, updated_at = NOW() WHERE user_id = $4',
    ['cancelling', subscription.current_period_end, 'customer_request', userId]
  );

  return {
    status: 'cancelling',
    accessUntil: subscription.current_period_end,
    message: 'Your subscription will remain active until ' + formatDate(subscription.current_period_end),
  };
}

// Middleware checks access_until, not just status
async function subscriptionMiddleware(req, res, next) {
  var userId = req.user.id;
  var billing = await db.query(
    'SELECT status, access_until FROM billing_accounts WHERE user_id = $1',
    [userId]
  );

  if (billing.rows.length === 0) {
    req.subscriptionActive = false;
    return next();
  }

  var row = billing.rows[0];

  if (row.status === 'active') {
    req.subscriptionActive = true;
  } else if (row.status === 'cancelling' && new Date(row.access_until) > new Date()) {
    req.subscriptionActive = true;
    req.cancellingAt = row.access_until;
  } else {
    req.subscriptionActive = false;
  }

  next();
}

Run a daily job to find subscriptions where access_until has passed and transition them from "cancelling" to "cancelled":

async function processExpiredCancellations() {
  await db.query(
    'UPDATE billing_accounts SET status = $1, updated_at = NOW() WHERE status = $2 AND access_until < NOW()',
    ['cancelled', 'cancelling']
  );
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Implementing Pause (Without a Native Feature)

Paystack has no pause button. Pausing means: stop charging temporarily, let the customer resume later without re-entering card details.

Here is the pattern:

async function pauseSubscription(userId, pauseReason) {
  var subscription = await getActiveSubscription(userId);

  // Cancel on Paystack
  await cancelSubscription(
    subscription.paystack_subscription_code,
    subscription.paystack_email_token
  );

  // Store the pause state in your database
  // Keep the authorization code for later resumption
  await db.query(
    'UPDATE billing_accounts SET status = $1, paused_at = NOW(), pause_reason = $2, updated_at = NOW() WHERE user_id = $3',
    ['paused', pauseReason, userId]
  );

  return {
    status: 'paused',
    message: 'Subscription paused. You can resume anytime.',
  };
}

async function resumeSubscription(userId) {
  var billing = await db.query(
    'SELECT plan_id, payment_method_id FROM billing_accounts WHERE user_id = $1 AND status = $2',
    [userId, 'paused']
  );

  if (billing.rows.length === 0) {
    throw new Error('No paused subscription found');
  }

  var account = billing.rows[0];
  var paymentMethod = await getPaymentMethod(account.payment_method_id);
  var plan = await getPlan(account.plan_id);
  var user = await getUser(userId);

  // Check if the card is still valid
  var now = new Date();
  if (paymentMethod.exp_year < now.getFullYear() ||
      (paymentMethod.exp_year === now.getFullYear() && paymentMethod.exp_month < now.getMonth() + 1)) {
    return {
      success: false,
      reason: 'card_expired',
      message: 'Your card has expired. Please add a new card to resume.',
    };
  }

  // Create new subscription on Paystack
  var authCode = await getAuthorizationCode(account.payment_method_id);

  var response = await axios.post(
    'https://api.paystack.co/subscription',
    {
      customer: user.email,
      plan: plan.paystack_plan_code,
      authorization: authCode,
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  var newSub = response.data.data;

  await db.query(
    'UPDATE billing_accounts SET status = $1, paused_at = NULL, paystack_subscription_code = $2, paystack_email_token = $3, updated_at = NOW() WHERE user_id = $4',
    ['active', newSub.subscription_code, newSub.email_token, userId]
  );

  return {
    success: true,
    message: 'Subscription resumed.',
    nextCharge: newSub.next_payment_date,
  };
}

Downsides of the cancel-and-resume approach:

  • The billing date resets when you create a new subscription.
  • Subscription history (invoices_count, creation date) resets.
  • The card may have expired during the pause.

Setting Pause Limits

Allow unlimited pauses and some customers will stay paused forever. Set limits.

var PAUSE_POLICY = {
  maxPausesPerYear: 3,
  maxPauseDurationDays: 30,
  minimumActiveBeforePause: 30, // Must be active for at least 30 days before pausing
};

async function canPause(userId) {
  // Check pause count this year
  var yearStart = new Date(new Date().getFullYear(), 0, 1);
  var pauseCount = await db.query(
    'SELECT COUNT(*) as count FROM pause_history WHERE user_id = $1 AND paused_at >= $2',
    [userId, yearStart]
  );

  if (parseInt(pauseCount.rows[0].count) >= PAUSE_POLICY.maxPausesPerYear) {
    return { allowed: false, reason: 'Maximum ' + PAUSE_POLICY.maxPausesPerYear + ' pauses per year reached.' };
  }

  // Check minimum active time
  var billing = await db.query(
    'SELECT status, updated_at FROM billing_accounts WHERE user_id = $1',
    [userId]
  );

  if (billing.rows.length > 0) {
    var daysSinceLastChange = Math.floor(
      (Date.now() - new Date(billing.rows[0].updated_at).getTime()) / (24 * 60 * 60 * 1000)
    );

    if (daysSinceLastChange < PAUSE_POLICY.minimumActiveBeforePause) {
      return { allowed: false, reason: 'Subscription must be active for at least ' + PAUSE_POLICY.minimumActiveBeforePause + ' days before pausing.' };
    }
  }

  return { allowed: true };
}

Auto-resume after the maximum pause duration to prevent indefinite pauses:

// Run daily
async function autoResumePausedSubscriptions() {
  var maxPauseDate = new Date(Date.now() - PAUSE_POLICY.maxPauseDurationDays * 24 * 60 * 60 * 1000);

  var overduePauses = await db.query(
    'SELECT user_id FROM billing_accounts WHERE status = $1 AND paused_at < $2',
    ['paused', maxPauseDate]
  );

  for (var i = 0; i < overduePauses.rows.length; i++) {
    try {
      await resumeSubscription(overduePauses.rows[i].user_id);
    } catch (error) {
      console.log('Auto-resume failed for user ' + overduePauses.rows[i].user_id + ': ' + error.message);
    }
  }
}

The Reactivation Flow

A cancelled customer comes back. They want to resubscribe. The flow depends on whether their old authorization is still valid.

async function reactivate(userId, planCode) {
  var user = await getUser(userId);
  var paymentMethod = await getDefaultPaymentMethod(userId);

  if (!paymentMethod || !paymentMethod.is_reusable) {
    // No valid card on file. Send them through checkout.
    var checkoutUrl = await initializeSubscriptionCheckout(user.email, planCode);
    return {
      needsCard: true,
      checkoutUrl: checkoutUrl,
    };
  }

  // Check card expiry
  var now = new Date();
  var expired = paymentMethod.exp_year < now.getFullYear() ||
    (paymentMethod.exp_year === now.getFullYear() && paymentMethod.exp_month < now.getMonth() + 1);

  if (expired) {
    var checkoutUrl = await initializeSubscriptionCheckout(user.email, planCode);
    return {
      needsCard: true,
      reason: 'Your saved card has expired.',
      checkoutUrl: checkoutUrl,
    };
  }

  // Card is valid. Create subscription directly.
  var authCode = await getAuthorizationCode(paymentMethod.id);
  var subscription = await createPaystackSubscription(user.email, planCode, authCode);

  await db.query(
    'UPDATE billing_accounts SET status = $1, plan_id = (SELECT id FROM billing_plans WHERE paystack_plan_code = $2), paystack_subscription_code = $3, paystack_email_token = $4, cancelled_at = NULL, updated_at = NOW() WHERE user_id = $5',
    ['active', planCode, subscription.subscription_code, subscription.email_token, userId]
  );

  return {
    needsCard: false,
    status: 'active',
    nextCharge: subscription.next_payment_date,
  };
}

For customers who cancelled voluntarily, consider offering a win-back discount on reactivation. See coupons and discounts.

Tracking Cancellation Reasons

When a customer cancels, ask why. This data drives product improvements and retention strategies.

var CANCELLATION_REASONS = [
  { id: 'too_expensive', label: 'Too expensive' },
  { id: 'not_using', label: 'Not using it enough' },
  { id: 'missing_features', label: 'Missing features I need' },
  { id: 'found_alternative', label: 'Found a better alternative' },
  { id: 'temporary', label: 'Just need a break (will come back)' },
  { id: 'other', label: 'Other' },
];

async function processCancellationWithReason(userId, reasonId, feedback) {
  await db.query(
    'INSERT INTO cancellation_records (user_id, reason, feedback, created_at) VALUES ($1, $2, $3, NOW())',
    [userId, reasonId, feedback]
  );

  // Offer retention based on reason
  if (reasonId === 'too_expensive') {
    // Offer a discount or suggest a cheaper plan
    return {
      retention_offer: 'downgrade',
      message: 'Would a lower plan work better for your budget?',
    };
  }

  if (reasonId === 'temporary') {
    return {
      retention_offer: 'pause',
      message: 'You can pause your subscription instead of cancelling.',
    };
  }

  // Proceed with cancellation
  return await requestCancellation(userId);
}

Track "too expensive" and "temporary" separately. For "too expensive," you might retain the customer with a downgrade or discount. For "temporary," offer pause instead of full cancellation. These are low-cost retention strategies that reduce churn significantly. For churn analytics, see subscription churn instrumentation.

Key Takeaways

  • ✓Paystack cancels subscriptions via the disable endpoint. It requires the subscription_code and email_token.
  • ✓Disabling a subscription stops all future charges immediately. Paystack does not pro-rate or refund the current period.
  • ✓End-of-period cancellation is a business decision you implement. Disable the subscription but let the customer keep access until current_period_end.
  • ✓Paystack has no native pause feature. Implement pause by cancelling the subscription and storing the authorization for later reactivation.
  • ✓Reactivation creates a new subscription using the stored authorization code. The billing date resets to the reactivation date.
  • ✓Track voluntary vs involuntary cancellations separately. They need different retention strategies.

Frequently Asked Questions

Does Paystack support pausing a subscription?
No. Paystack has no native pause feature. You implement pause by disabling the subscription (stopping charges), storing the authorization code, and creating a new subscription when the customer wants to resume.
What happens to the billing date when I cancel and resume a subscription?
The billing date resets to the date you create the new subscription. If the original subscription was billed on the 1st and you resume on the 15th, the new billing date is the 15th.
Can the customer cancel their own subscription without going through my app?
Yes, if they have the email_token. Paystack sends subscription management emails that include a link for the customer to cancel directly through Paystack. You can disable these emails if you want all cancellations to go through your app. See the subscription emails article for details.
Should I let customers cancel immediately or only at end of period?
End-of-period cancellation is the industry standard. The customer keeps access until the end of their paid period. This is fairer (they paid for the full period) and reduces disputes. Store the cancellation date and access_until date separately in your database.
How do I re-enable a disabled Paystack subscription?
Call POST /subscription/enable with the subscription code and email token. This reactivates the subscription and resumes automatic charges. Note that this only works if the subscription was recently disabled. For long-paused subscriptions, creating a new subscription is more reliable.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Subscriptions and Recurring Billing: Complete Guide

Everything you need to build recurring billing on Paystack. Covers the subscriptions API, plans, authorization codes for manual charges, dunning, lifecycle webhooks, cancellation, and real-world patterns for SaaS, memberships, school fees, and loan collection across Africa.

Read more

Handling Failed Recurring Charges and Dunning on Paystack

Recurring charges fail. Cards expire, accounts run dry, banks decline for opaque reasons. This guide covers every failure scenario in Paystack recurring billing, shows you how to read gateway responses, build a retry strategy, implement a dunning state machine, and handle the higher failure rates typical in African card markets.

Read more

Free Trials with Paystack Subscriptions

Paystack does not have a built-in trial period field. You implement trials yourself. This guide covers three patterns: no-card trials managed in your app, card-first trials with deferred subscription creation, and zero-amount verification charges. Includes code for trial management, conversion tracking, and the webhook flows that tie trials to paid subscriptions.

Read more

Subscription Churn Instrumentation on Paystack Data

Churn is the slow leak that kills subscription businesses. This guide shows how to use Paystack subscription events — subscription.disable, invoice.payment_failed, and charge.success — to calculate MRR, track churn rate, segment by failure reason, and build a recovery flow.

Read more

Paystack Invoice Events for Subscription Lifecycles

Paystack subscription lifecycles are driven by webhook events. This guide documents every event type, shows the payload structure, explains when each fires, and provides handler code for building a robust subscription state machine on top of Paystack invoice and subscription webhooks.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.