Paystack plan amounts cannot be changed after creation. To raise prices, create a new plan at the new price and direct new subscribers to it. Existing subscribers stay on the old plan at the old price (grandfathering). To eventually migrate them, cancel their old subscription and create a new one on the new plan with advance notice. Archive the old plan when no subscribers remain.
Once you create a Paystack plan with an amount and interval, those fields cannot be changed. This is by design. Changing a plan's price would silently change the charge amount for every subscriber, which could be catastrophic.
This immutability means price changes require a new plan. Old plan for old price. New plan for new price. Your application manages the mapping between them.
This article is part of the subscriptions and recurring billing guide.
When you raise prices, existing subscribers stay on the old plan at the old price. New subscribers sign up on the new plan at the new price. This is grandfathering.
// Step 1: Create the new plan
async function createNewPricePlan() {
var response = await axios.post(
'https://api.paystack.co/plan',
{
name: 'Pro Monthly v2',
amount: 700000, // New price: 7,000 NGN (was 5,000)
interval: 'monthly',
currency: 'NGN',
send_invoices: false,
},
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
}
);
return response.data.data.plan_code;
}
// Step 2: Archive the old plan (prevent new signups)
async function archiveOldPlan(oldPlanCode) {
// Note: archiving via the Paystack Dashboard is most reliable
// Existing subscriptions on this plan continue billing
console.log('Archive plan ' + oldPlanCode + ' via Dashboard');
}
// Step 3: Update your application config
var PLAN_MAP = {
pro_monthly: {
current: 'PLN_pro_monthly_v2_xxx', // New subscribers get this
legacy: ['PLN_pro_monthly_v1_xxx'], // Old subscribers are on these
amount: 700000,
legacyAmounts: { 'PLN_pro_monthly_v1_xxx': 500000 },
},
};
Key steps:
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
CREATE TABLE plan_versions (
id SERIAL PRIMARY KEY,
tier VARCHAR(50) NOT NULL,
version INTEGER NOT NULL,
paystack_plan_code VARCHAR(50) UNIQUE NOT NULL,
amount INTEGER NOT NULL,
interval VARCHAR(20) NOT NULL,
currency VARCHAR(3) NOT NULL,
is_current BOOLEAN DEFAULT false,
is_archived BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
archived_at TIMESTAMPTZ
);
-- Example data:
-- tier: pro, version: 1, code: PLN_xxx_v1, amount: 500000, is_current: false, is_archived: true
-- tier: pro, version: 2, code: PLN_xxx_v2, amount: 700000, is_current: true, is_archived: false
async function getCurrentPlanCode(tier, interval) {
var result = await db.query(
'SELECT paystack_plan_code FROM plan_versions WHERE tier = $1 AND interval = $2 AND is_current = true',
[tier, interval]
);
return result.rows[0].paystack_plan_code;
}
async function isLegacyPlan(planCode) {
var result = await db.query(
'SELECT is_current FROM plan_versions WHERE paystack_plan_code = $1',
[planCode]
);
return result.rows.length > 0 && !result.rows[0].is_current;
}
// In your billing dashboard, show whether the customer is on a legacy plan
async function getBillingDisplay(userId) {
var billing = await getCurrentBilling(userId);
var planVersion = await db.query(
'SELECT tier, version, amount, is_current FROM plan_versions WHERE paystack_plan_code = $1',
[billing.plan_code]
);
var pv = planVersion.rows[0];
var isLegacy = !pv.is_current;
return {
planName: pv.tier + ' (v' + pv.version + ')',
amount: pv.amount,
isLegacyPrice: isLegacy,
legacyMessage: isLegacy ? 'You are on a legacy price. Current price for new subscribers is higher.' : null,
};
}
Sometimes you need to move grandfathered subscribers to the new price. This should be rare and handled carefully.
async function migrateToNewPrice(userId, newPlanCode) {
var user = await getUser(userId);
var billing = await getCurrentBilling(userId);
var paymentMethod = await getDefaultPaymentMethod(userId);
// Cancel old subscription at end of current period
await disableSubscription(billing.paystack_subscription_code, billing.paystack_email_token);
// Schedule migration for end of current period
await db.query(
'UPDATE billing_accounts SET pending_plan_change = $1, pending_change_date = $2, migration_reason = $3, updated_at = NOW() WHERE user_id = $4',
[newPlanCode, billing.current_period_end, 'price_increase', userId]
);
return {
migrationDate: billing.current_period_end,
newPlan: newPlanCode,
};
}
// Batch migration: migrate all subscribers on a legacy plan
async function batchMigrate(legacyPlanCode, newPlanCode) {
var subscribers = await db.query(
'SELECT ba.user_id FROM billing_accounts ba JOIN plan_versions pv ON ba.plan_code = pv.paystack_plan_code WHERE pv.paystack_plan_code = $1 AND ba.status = $2',
[legacyPlanCode, 'active']
);
console.log('Migrating ' + subscribers.rows.length + ' subscribers from ' + legacyPlanCode + ' to ' + newPlanCode);
for (var i = 0; i < subscribers.rows.length; i++) {
try {
await migrateToNewPrice(subscribers.rows[i].user_id, newPlanCode);
} catch (error) {
console.log('Migration failed for user ' + subscribers.rows[i].user_id + ': ' + error.message);
}
}
}
Always give at least 30 days notice before migrating subscribers to a higher price. 60 days is better. Send multiple notifications: 60 days, 30 days, 14 days, and 3 days before the change.
Price increase emails should lead with value, not with the price change. Here is a template:
Subject: Changes to your [Product] plan
Structure:
For loyal customers (subscribed for 12+ months), consider offering a transition period: 3 months at the old price before the new price kicks in. This rewards loyalty and reduces churn from the price increase.
Track churn rates in the 30-60 days following a price increase. Compare to your normal churn rate. If the spike is significant, your price increase may be too aggressive or your communication may not have conveyed enough value.
Grandfathering forever is not always sustainable. If your costs increase significantly, grandfathered subscribers at old prices can become unprofitable.
Decide based on:
A common middle ground: grandfather the price for 12-24 months, then migrate to the new price with advance notice. This rewards early loyalty while ensuring pricing sustainability.
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