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.
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:
subscription.disable webhook event.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.
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.
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:
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);
}
}
}
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.
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.
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