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. Coupons and Discounts on Paystack Subscriptions
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Coupons and Discounts on Paystack Subscriptions

Paystack has no built-in coupon or discount feature. You have two options: create separate Paystack plans at discounted prices and subscribe customers to those plans, or manage coupons in your application and use charge_authorization to charge discounted amounts. The second approach is more flexible but requires building your own billing engine. For simple "first month free" or "20% off first 3 months" promotions, the discounted plan approach is easier.

In this article

  1. 1. The Coupon Challenge on Paystack
  2. 2. Approach 1: Discounted Plans
  3. 3. Approach 2: Application-Managed Coupons
  4. 4. Common Promotion Patterns
  5. 5. Migrating Customers After the Promotion Ends
  6. 6. Tracking Coupon Performance

The Coupon Challenge on Paystack

On Stripe, you create a coupon, attach it to a subscription, and Stripe applies the discount automatically. On Paystack, none of this exists. You build the entire coupon system yourself.

Two approaches:

  1. Discounted plans: Create a separate Paystack plan at the promotional price. Subscribe promotional customers to this plan. Simple but creates plan sprawl.
  2. Application-managed coupons: Store coupons in your database, validate them at checkout, and use charge_authorization with the discounted amount. Flexible but requires your own billing engine.

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

Approach 1: Discounted Plans

Create a Paystack plan at the discounted price and subscribe promotional customers to it.

// Create a promotional plan
async function createPromoPlan() {
  var response = await axios.post(
    'https://api.paystack.co/plan',
    {
      name: 'Pro Monthly - Launch Promo 50% Off (3 months)',
      amount: 250000, // 2,500 NGN instead of 5,000 NGN
      interval: 'monthly',
      currency: 'NGN',
      invoice_limit: 3, // Only 3 charges at this price
      send_invoices: false,
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data.data.plan_code;
}

// Subscribe customer to the promo plan
async function subscribeWithPromo(email, promoPlanCode) {
  var response = await axios.post(
    'https://api.paystack.co/transaction/initialize',
    {
      email: email,
      amount: 250000,
      plan: promoPlanCode,
      callback_url: 'https://yoursite.com/billing/callback',
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data.data.authorization_url;
}

When the invoice_limit is reached, the subscription completes. Listen for the subscription.disable webhook and migrate the customer to the full-price plan.

Pros: Simple. Paystack handles the billing. No custom billing engine needed.

Cons: Creates many plans. Each unique discount requires a separate plan. Hard to manage at scale.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Approach 2: Application-Managed Coupons

CREATE TABLE coupons (
  id SERIAL PRIMARY KEY,
  code VARCHAR(50) UNIQUE NOT NULL,
  discount_type VARCHAR(20) NOT NULL, -- 'percentage' or 'fixed'
  discount_value INTEGER NOT NULL,
  max_redemptions INTEGER,
  current_redemptions INTEGER DEFAULT 0,
  valid_from TIMESTAMPTZ,
  valid_until TIMESTAMPTZ,
  applicable_plans TEXT[], -- NULL means all plans
  duration_months INTEGER, -- How many months the discount lasts
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE coupon_redemptions (
  id SERIAL PRIMARY KEY,
  coupon_id INTEGER REFERENCES coupons(id),
  user_id INTEGER REFERENCES users(id),
  redeemed_at TIMESTAMPTZ DEFAULT NOW(),
  months_remaining INTEGER NOT NULL
);
async function validateCoupon(code, planId) {
  var coupon = await db.query(
    'SELECT * FROM coupons WHERE code = $1 AND is_active = true',
    [code.toUpperCase()]
  );

  if (coupon.rows.length === 0) {
    return { valid: false, reason: 'Invalid coupon code' };
  }

  var c = coupon.rows[0];

  // Check date validity
  var now = new Date();
  if (c.valid_from && new Date(c.valid_from) > now) {
    return { valid: false, reason: 'This coupon is not yet active' };
  }
  if (c.valid_until && new Date(c.valid_until) < now) {
    return { valid: false, reason: 'This coupon has expired' };
  }

  // Check redemption limit
  if (c.max_redemptions && c.current_redemptions >= c.max_redemptions) {
    return { valid: false, reason: 'This coupon has been fully redeemed' };
  }

  // Check plan applicability
  if (c.applicable_plans && c.applicable_plans.indexOf(planId) === -1) {
    return { valid: false, reason: 'This coupon does not apply to this plan' };
  }

  return {
    valid: true,
    coupon: c,
    discountType: c.discount_type,
    discountValue: c.discount_value,
    durationMonths: c.duration_months,
  };
}

function applyDiscount(originalAmount, coupon) {
  if (coupon.discountType === 'percentage') {
    var discount = Math.round(originalAmount * coupon.discountValue / 100);
    return originalAmount - discount;
  }
  if (coupon.discountType === 'fixed') {
    return Math.max(0, originalAmount - coupon.discountValue);
  }
  return originalAmount;
}

With application-managed coupons, you use charge_authorization instead of the Subscriptions API. This gives you full control over the amount but requires you to build the billing engine described in building your own billing engine.

Common Promotion Patterns

First month free

Collect card details during signup (see free trials). Start the subscription one month later. The customer gets a free month and then pays full price.

20% off for 3 months

With discounted plans: create a plan at 80% of the price with invoice_limit: 3. After 3 charges, migrate to the full-price plan.

With application coupons: apply the 20% discount in your billing calculator for 3 months, then charge full price from month 4.

Annual discount

This is not really a coupon but a pricing strategy. Create an annual plan priced lower per month than the monthly plan. See annual vs monthly billing.

Referral credit

When user A refers user B, give both a credit. Store the credit in your database and apply it to the next charge. This is best handled with application-managed credits rather than coupons.

Win-back discount

When a cancelled customer returns, offer them a discount on the first 1-2 months. Use either approach, but application-managed coupons are more flexible here because you can target specific users.

Migrating Customers After the Promotion Ends

// Listen for subscription completion (invoice_limit reached on promo plan)
async function handlePromoSubscriptionComplete(subscriptionCode) {
  var billing = await db.query(
    'SELECT user_id, plan_id FROM billing_accounts WHERE paystack_subscription_code = $1',
    [subscriptionCode]
  );

  var userId = billing.rows[0].user_id;
  var user = await getUser(userId);
  var paymentMethod = await getDefaultPaymentMethod(userId);

  if (!paymentMethod) {
    await sendPromoEndedNoCardEmail(user.email);
    return;
  }

  // Create subscription on the full-price plan
  var fullPricePlanCode = getFullPricePlanCode(billing.rows[0].plan_id);
  var authCode = await getAuthorizationCode(paymentMethod.id);

  try {
    var newSub = await createPaystackSubscription(user.email, fullPricePlanCode, authCode);

    await db.query(
      'UPDATE billing_accounts SET plan_id = (SELECT id FROM billing_plans WHERE paystack_plan_code = $1), paystack_subscription_code = $2, paystack_email_token = $3, updated_at = NOW() WHERE user_id = $4',
      [fullPricePlanCode, newSub.subscription_code, newSub.email_token, userId]
    );

    await sendPromoEndedNewPriceEmail(user.email, fullPricePlanCode);
  } catch (error) {
    console.log('Failed to migrate user ' + userId + ' to full price: ' + error.message);
    await sendPromoMigrationFailedEmail(user.email);
  }
}

Notify the customer before the transition. Send an email 7 days before the promo ends: "Your promotional pricing ends on [date]. Starting next month, your plan will be [full price]." This reduces churn from price shock.

Tracking Coupon Performance

async function getCouponMetrics(couponId) {
  var coupon = await db.query('SELECT * FROM coupons WHERE id = $1', [couponId]);
  var c = coupon.rows[0];

  var redemptions = await db.query(
    'SELECT COUNT(*) as count FROM coupon_redemptions WHERE coupon_id = $1',
    [couponId]
  );

  var stillActive = await db.query(
    'SELECT COUNT(*) as count FROM coupon_redemptions cr JOIN billing_accounts ba ON cr.user_id = ba.user_id WHERE cr.coupon_id = $1 AND ba.status = $2',
    [couponId, 'active']
  );

  var churned = await db.query(
    'SELECT COUNT(*) as count FROM coupon_redemptions cr JOIN billing_accounts ba ON cr.user_id = ba.user_id WHERE cr.coupon_id = $1 AND ba.status IN ($2, $3)',
    [couponId, 'cancelled', 'suspended']
  );

  return {
    code: c.code,
    totalRedemptions: parseInt(redemptions.rows[0].count),
    stillActive: parseInt(stillActive.rows[0].count),
    churned: parseInt(churned.rows[0].count),
    retentionRate: parseInt(redemptions.rows[0].count) > 0
      ? (parseInt(stillActive.rows[0].count) / parseInt(redemptions.rows[0].count) * 100).toFixed(1) + '%'
      : '0%',
  };
}

The key metric is retention after the discount ends. If customers churn when full pricing kicks in, the promotion attracted price-sensitive users, not value-driven users. Adjust your promotion strategy based on this data.

Key Takeaways

  • ✓Paystack has no native coupon or discount system. You build it yourself.
  • ✓The simplest approach: create a discounted Paystack plan and subscribe promotional customers to it.
  • ✓The flexible approach: manage coupons in your database and use charge_authorization with the discounted amount.
  • ✓For limited-duration discounts, use the plan's invoice_limit to cap discounted charges, then migrate the customer to the full-price plan.
  • ✓Track coupon usage, redemption rates, and the revenue impact of each promotion.
  • ✓Always validate coupons server-side. Never trust a discount amount from the frontend.

Frequently Asked Questions

Does Paystack have a coupon or discount API?
No. Paystack does not have native coupon, discount, or promotional pricing features. You build coupon logic in your own application using either discounted plans or application-managed coupon codes with charge_authorization.
Which approach is better: discounted plans or application-managed coupons?
Discounted plans are simpler and work with the Subscriptions API. Use them for a small number of promotions. Application-managed coupons are more flexible and scalable but require a custom billing engine. Use them if you run frequent, varied promotions.
How do I prevent coupon abuse?
Limit redemptions per coupon (max_redemptions), limit to one redemption per user, set expiry dates, validate server-side (never trust the frontend), and track redemption patterns. Flag users who create multiple accounts to use the same coupon.
Can I apply a coupon to an existing subscription?
With the discounted plan approach, you need to cancel the current subscription and create a new one on the discounted plan. With application-managed coupons and charge_authorization, you apply the discount to the next charge without changing the subscription.

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

Proration and Plan Upgrades with Paystack

Paystack does not handle proration automatically. When a customer upgrades or downgrades mid-cycle, you calculate the prorated amount, charge or credit the difference, cancel the old subscription, and create a new one. This guide provides the math, the code, and the edge cases for implementing proration on Paystack.

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

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

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.