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. Proration and Plan Upgrades with Paystack
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Proration and Plan Upgrades with Paystack

To prorate a plan change on Paystack: calculate the unused portion of the old plan, calculate the cost of the new plan for the remaining days, charge the difference via charge_authorization (for upgrades) or store a credit (for downgrades), then cancel the old subscription and create a new one on the new plan. Paystack does not do any of this for you.

In this article

  1. 1. Why Proration Matters
  2. 2. The Proration Math
  3. 3. The Upgrade Flow
  4. 4. The Downgrade Flow
  5. 5. Applying Credits on the Next Charge
  6. 6. The Cancel-and-Resubscribe Pattern
  7. 7. Edge Cases in Proration

Why Proration Matters

A customer on your Basic plan (2,000 NGN/month) wants to upgrade to Pro (5,000 NGN/month) on the 15th of the month, halfway through their billing cycle. What do they pay?

Without proration: You charge the full 5,000 NGN for Pro immediately, and the customer effectively loses the 1,000 NGN of unused Basic time. Or you wait until the next billing cycle, and the customer does not get Pro features for two weeks.

With proration: You calculate the unused Basic portion (about 1,000 NGN for the remaining 15 days), calculate the Pro cost for those 15 days (about 2,500 NGN), and charge the difference (about 1,500 NGN). The customer pays fairly for what they use.

Paystack does not do any of this for you. The Subscriptions API charges the plan amount on the billing date. It has no concept of mid-cycle changes or prorated amounts. You implement proration in your application.

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

The Proration Math

Proration is about calculating the daily rate for each plan and applying it to the remaining days in the billing period.

function calculateProration(params) {
  var oldPlanAmount = params.oldPlanAmount;
  var newPlanAmount = params.newPlanAmount;
  var periodStart = new Date(params.periodStart);
  var periodEnd = new Date(params.periodEnd);
  var changeDate = new Date(params.changeDate);

  // Total days in the billing period
  var totalDays = Math.ceil((periodEnd - periodStart) / (24 * 60 * 60 * 1000));

  // Days already used on the old plan
  var daysUsed = Math.ceil((changeDate - periodStart) / (24 * 60 * 60 * 1000));

  // Days remaining on the new plan
  var daysRemaining = totalDays - daysUsed;

  // Daily rates
  var oldDailyRate = Math.round(oldPlanAmount / totalDays);
  var newDailyRate = Math.round(newPlanAmount / totalDays);

  // Credit for unused old plan days
  var unusedCredit = oldDailyRate * daysRemaining;

  // Cost for remaining days on new plan
  var newPlanCost = newDailyRate * daysRemaining;

  // The amount to charge (positive) or credit (negative)
  var proratedAmount = newPlanCost - unusedCredit;

  return {
    totalDays: totalDays,
    daysUsed: daysUsed,
    daysRemaining: daysRemaining,
    unusedCredit: unusedCredit,
    newPlanCost: newPlanCost,
    proratedAmount: proratedAmount,
    isUpgrade: proratedAmount > 0,
  };
}

// Example: upgrade from Basic (200000 kobo) to Pro (500000 kobo)
// halfway through the month
var result = calculateProration({
  oldPlanAmount: 200000,
  newPlanAmount: 500000,
  periodStart: '2026-07-01',
  periodEnd: '2026-07-31',
  changeDate: '2026-07-15',
});

// result.proratedAmount would be approximately 160000 kobo (1,600 NGN)

Use Math.round on daily rates to avoid fractional kobo. Small rounding differences (1-2 kobo) are acceptable and expected. Always round in the customer's favor if you want to be generous, or consistently round up if you want to be precise.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

The Upgrade Flow

When a customer upgrades, you charge the prorated difference immediately and switch them to the new plan.

async function upgradePlan(userId, newPlanCode) {
  var user = await getUser(userId);
  var currentSub = await getActiveSubscription(userId);

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

  var oldPlan = await getPlan(currentSub.plan_id);
  var newPlan = await getPlanByCode(newPlanCode);

  if (newPlan.base_amount <= oldPlan.base_amount) {
    throw new Error('New plan must be more expensive for an upgrade. Use downgrade flow instead.');
  }

  // Calculate proration
  var proration = calculateProration({
    oldPlanAmount: oldPlan.base_amount,
    newPlanAmount: newPlan.base_amount,
    periodStart: currentSub.current_period_start,
    periodEnd: currentSub.current_period_end,
    changeDate: new Date(),
  });

  // Charge the prorated difference
  var paymentMethod = await getDefaultPaymentMethod(userId);
  var reference = 'UPGRADE_' + userId + '_' + Date.now();

  var chargeResult = await chargeAuthorization(
    user.email,
    proration.proratedAmount,
    paymentMethod.authorization_code,
    reference,
    oldPlan.currency
  );

  if (chargeResult.status !== 'success') {
    throw new Error('Proration charge failed: ' + chargeResult.gateway_response);
  }

  // Cancel old subscription on Paystack
  await disableSubscription(currentSub.paystack_subscription_code, currentSub.paystack_email_token);

  // Create new subscription on the upgraded plan
  var newSub = await createSubscription(user.email, newPlanCode, paymentMethod.authorization_code);

  // Update your database
  await db.query(
    'UPDATE billing_accounts SET plan_id = $1, updated_at = NOW() WHERE user_id = $2',
    [newPlan.id, userId]
  );

  return {
    proratedCharge: proration.proratedAmount,
    chargeReference: reference,
    newSubscription: newSub.subscription_code,
  };
}

If the prorated charge fails (insufficient funds, card declined), do not proceed with the plan switch. The customer stays on their current plan. Notify them that the upgrade could not be completed and suggest they ensure their card has sufficient funds.

The Downgrade Flow

Downgrades are trickier because the customer has overpaid. They are owed a credit for the difference between what they paid for the current period and what the lower plan costs.

async function downgradePlan(userId, newPlanCode) {
  var user = await getUser(userId);
  var currentSub = await getActiveSubscription(userId);
  var oldPlan = await getPlan(currentSub.plan_id);
  var newPlan = await getPlanByCode(newPlanCode);

  if (newPlan.base_amount >= oldPlan.base_amount) {
    throw new Error('New plan must be cheaper for a downgrade. Use upgrade flow instead.');
  }

  var proration = calculateProration({
    oldPlanAmount: oldPlan.base_amount,
    newPlanAmount: newPlan.base_amount,
    periodStart: currentSub.current_period_start,
    periodEnd: currentSub.current_period_end,
    changeDate: new Date(),
  });

  // proratedAmount is negative for downgrades
  var creditAmount = Math.abs(proration.proratedAmount);

  // Store the credit in the customer's account
  await db.query(
    'INSERT INTO billing_credits (user_id, amount, currency, reason, created_at, expires_at) VALUES ($1, $2, $3, $4, NOW(), $5)',
    [userId, creditAmount, oldPlan.currency, 'Proration credit from downgrade', null]
  );

  // Two options for when the downgrade takes effect:
  // Option A: Immediate. Switch now, credit the difference.
  await disableSubscription(currentSub.paystack_subscription_code, currentSub.paystack_email_token);
  var paymentMethod = await getDefaultPaymentMethod(userId);
  var newSub = await createSubscription(user.email, newPlanCode, paymentMethod.authorization_code);

  // Option B: End of period. Keep the higher plan until the period ends, then switch.
  // This avoids the credit complexity but delays the downgrade.

  await db.query(
    'UPDATE billing_accounts SET plan_id = $1, updated_at = NOW() WHERE user_id = $2',
    [newPlan.id, userId]
  );

  return {
    creditAmount: creditAmount,
    newPlan: newPlan.name,
  };
}

Most SaaS products use Option B for downgrades: the customer keeps their higher-tier features until the end of the current billing period, then switches to the lower plan at the next renewal. This avoids issuing credits and simplifies the flow. Choose the option that makes sense for your business.

Applying Credits on the Next Charge

If you chose immediate downgrade with credits, apply the credit to the next billing cycle.

async function calculateChargeWithCredits(userId, planAmount) {
  // Get available credits
  var credits = await db.query(
    'SELECT id, amount FROM billing_credits WHERE user_id = $1 AND used_at IS NULL AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY created_at ASC',
    [userId]
  );

  var totalCredits = 0;
  var creditsToApply = [];

  for (var i = 0; i < credits.rows.length; i++) {
    var credit = credits.rows[i];
    var remaining = planAmount - totalCredits;

    if (remaining <= 0) break;

    var applyAmount = Math.min(credit.amount, remaining);
    totalCredits = totalCredits + applyAmount;
    creditsToApply.push({ id: credit.id, amount: applyAmount });
  }

  var chargeAmount = planAmount - totalCredits;

  return {
    originalAmount: planAmount,
    creditsApplied: totalCredits,
    chargeAmount: Math.max(0, chargeAmount),
    creditRecords: creditsToApply,
  };
}

// After a successful charge, mark credits as used
async function markCreditsUsed(creditRecords) {
  for (var i = 0; i < creditRecords.length; i++) {
    await db.query(
      'UPDATE billing_credits SET used_at = NOW() WHERE id = $1',
      [creditRecords[i].id]
    );
  }
}

If the credit covers the full charge amount, skip the Paystack charge but still create an invoice for record-keeping. Mark the invoice as paid with a note that it was covered by credits.

The Cancel-and-Resubscribe Pattern

Because Paystack plans are immutable (you cannot change the amount on an existing plan), a plan change always requires:

  1. Cancel (disable) the old subscription
  2. Create a new subscription on the new plan
async function switchSubscription(userId, newPlanCode) {
  var currentSub = await getActiveSubscription(userId);
  var user = await getUser(userId);
  var paymentMethod = await getDefaultPaymentMethod(userId);

  // Cancel old subscription
  await axios.post(
    'https://api.paystack.co/subscription/disable',
    {
      code: currentSub.paystack_subscription_code,
      token: currentSub.paystack_email_token,
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

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

  var newSub = response.data.data;

  // Update your database
  await db.query(
    'UPDATE billing_accounts SET paystack_subscription_code = $1, paystack_email_token = $2, updated_at = NOW() WHERE user_id = $3',
    [newSub.subscription_code, newSub.email_token, userId]
  );

  return newSub;
}

Be aware that when you create a new subscription, the billing date resets. If the customer was billed on the 1st of the month and you switch them mid-month, the new subscription's billing date is the day you created it, not the 1st. Plan your proration calculations accordingly.

Edge Cases in Proration

These edge cases trip up billing systems regularly:

Multiple plan changes in one billing period. A customer upgrades from Basic to Pro, then upgrades again from Pro to Enterprise. Calculate each proration independently. The second proration should be based on what they are currently paying (Pro), not the original plan (Basic).

Plan change on the first or last day. If the customer changes on day 1 of the billing period, the proration should be approximately the full difference. If they change on the last day, the proration should be nearly zero. Make sure your math handles both extremes correctly.

Short months. February has 28-29 days, other months have 30-31. Use actual calendar days, not a fixed 30-day assumption. The daily rate for a 5,000 NGN plan in February (28 days) is different from July (31 days).

Annual to monthly switch. If a customer on an annual plan wants to switch to monthly billing, the proration math involves different time scales. Calculate the daily rate of the annual plan, determine the unused days, and credit that amount. Then start monthly billing. See annual vs monthly billing logic.

Failed proration charge. If the prorated upgrade charge fails, do not switch the plan. The customer stays on the old plan. Show them a clear error message and suggest ensuring their card has sufficient funds. Do not leave the customer in a half-switched state.

Key Takeaways

  • ✓Paystack does not handle proration. You calculate prorated amounts and manage the plan switch yourself.
  • ✓For upgrades, charge the prorated difference immediately via charge_authorization. The customer pays only for the remaining days at the higher rate.
  • ✓For downgrades, store a credit for the unused portion of the higher plan. Apply it to the next billing cycle on the lower plan.
  • ✓The plan switch requires cancelling the old subscription and creating a new one. Paystack plans are immutable, so you cannot change a plan's price.
  • ✓Always calculate proration based on calendar days remaining in the billing period, not based on percentages of the billing interval.
  • ✓Document your proration policy clearly for customers. Mid-cycle changes are a common source of billing disputes.

Frequently Asked Questions

Does Paystack handle proration automatically?
No. Paystack has no proration feature. You calculate prorated amounts in your application, charge or credit the difference, and manage the subscription switch yourself by cancelling the old subscription and creating a new one.
Should I prorate downgrades or wait until the end of the billing period?
Most SaaS products wait until the end of the billing period for downgrades. The customer keeps their higher-tier features for the time they already paid for, and the lower plan starts at the next billing cycle. This avoids the complexity of calculating and managing credits.
What happens to the billing date when I cancel and recreate a subscription?
The billing date resets to the creation date of the new subscription. If you want the billing date to stay the same, you need to manage that in your application logic, possibly by using manual charge_authorization instead of the Subscriptions API.
How do I handle proration for annual plans?
Calculate the daily rate of the annual plan (annual amount divided by 365), multiply by remaining days, and credit that amount. Annual proration involves larger credits and charges, so make the amounts clear to the customer before they confirm the change.

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

Building a SaaS Billing Page on Paystack

Your SaaS billing page is where customers choose plans, manage subscriptions, view invoices, and update payment methods. This guide covers the complete implementation: Paystack plan setup, the pricing page, the checkout flow, the account billing dashboard, upgrade/downgrade logic, and the APIs that connect it all.

Read more

Grandfathering Old Prices on Paystack Plans

Paystack plans are immutable: you cannot change the amount. When you raise prices, existing subscribers stay on the old plan at the old price unless you explicitly migrate them. This guide covers plan versioning, the grandfather strategy, how to migrate subscribers when needed, and communicating price changes.

Read more

Annual vs Monthly Billing Logic on Paystack

Offering both annual and monthly billing increases revenue and reduces churn. This guide covers creating Paystack plans for both intervals, building the pricing page toggle, handling switches between annual and monthly, proration calculations, and the business logic around annual discounts.

Read more

Cancelling and Pausing Paystack Subscriptions

Paystack lets you cancel subscriptions but has no native pause feature. This guide covers the disable API for cancellation, the difference between immediate and end-of-period cancellation, how to implement pause and resume in your application, and the reactivation flow for customers who come back.

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.