Grandfathering Old Prices on Paystack Plans
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.
Why Paystack Plans Are Immutable
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.
The Grandfathering Strategy
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:
- Create the new plan at the new price.
- Update your pricing page and checkout flow to use the new plan code.
- Archive the old plan so no new subscribers can sign up for it.
- Existing subscribers continue paying the old price with no action needed.
Tracking Plan Versions
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,
};
}
Migrating Grandfathered Subscribers
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.
Communicating Price Changes
Price increase emails should lead with value, not with the price change. Here is a template:
Subject: Changes to your [Product] plan
Structure:
- Thank them for being a customer (especially long-term ones).
- Highlight what you have added since they subscribed (new features, improvements, reliability).
- State the price change clearly: old price, new price, effective date.
- Acknowledge the impact: "We understand this is a change."
- Offer alternatives: downgrade to a cheaper plan, switch to annual billing for savings, or contact support for special circumstances.
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.
When to End Grandfathering
Grandfathering forever is not always sustainable. If your costs increase significantly, grandfathered subscribers at old prices can become unprofitable.
Decide based on:
- Revenue impact: How much revenue are you losing to grandfathered pricing? Is it material?
- Customer lifetime: Long-term customers who have been paying for years deserve more consideration than someone who signed up last month at the old price.
- Competitive landscape: If competitors charge more, you have room to increase. If they charge less, the increase risks losing customers.
- Feature parity: If grandfathered customers have access to features that now cost more, migration is easier to justify.
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.
Key Takeaways
- ✓Paystack plan amounts are immutable. You cannot change the price of an existing plan. Create a new plan for the new price.
- ✓Existing subscribers stay on the old plan automatically. This is grandfathering by default.
- ✓Archive the old plan to prevent new signups while keeping existing subscriptions active.
- ✓To migrate grandfathered subscribers to the new price, give 30-60 days notice, then cancel the old subscription and create a new one.
- ✓Track plan versions in your database. Map old and new plan codes so your application can handle subscribers on different versions.
- ✓Price increases cause churn. Communicate the value increase alongside the price increase. Give loyal customers extra notice or a transition discount.
Frequently Asked Questions
- Can I change the price of an existing Paystack plan?
- No. Plan amount, interval, and currency are immutable on Paystack. To change the price, create a new plan and migrate subscribers. The old plan continues to charge the old amount until all subscribers are moved or cancel.
- What happens to existing subscribers when I archive a plan?
- Archiving a plan prevents new subscriptions but does not affect existing subscribers. They continue to be charged at the plan's original amount on their billing cycle. The plan remains active for them until they cancel or you migrate them.
- Should I notify customers before a price increase?
- Always. Give at least 30 days notice, ideally 60 days. Multiple notifications (60 days, 30 days, 14 days, 3 days) ensure the customer sees the message. Surprise price increases cause churn and damage trust.
- How do I handle annual subscribers during a price increase?
- Annual subscribers should keep their current price through the end of their annual term. The new price takes effect at renewal. This is both fair (they paid for the year) and legally prudent. Notify them well before the renewal date.
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