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. Subscription Churn Instrumentation on Paystack Data
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Subscription Churn Instrumentation on Paystack Data

Instrument churn by handling three Paystack events: subscription.disable (voluntary churn), invoice.payment_failed (involuntary churn from declined card), and charge.success (recovered). Store each event in a subscription_events table with timestamp and reason. Churn rate = cancellations this month / active subscriptions at start of month. Involuntary churn is recoverable — send an email with a card update link within 24 hours of a failed charge.

In this article

  1. 1. Subscription Events Table
  2. 2. Instrumenting from Webhook Events
  3. 3. Calculating Churn Rate and MRR
  4. 4. Learn More

Subscription Events Table

CREATE TABLE subscription_events (
  id SERIAL PRIMARY KEY,
  subscription_code TEXT NOT NULL,
  customer_email TEXT NOT NULL,
  plan_code TEXT,
  plan_name TEXT,
  plan_amount NUMERIC(12, 2),
  event_type TEXT NOT NULL,
  -- 'created' | 'renewed' | 'payment_failed' | 'cancelled' | 'reactivated'
  churn_type TEXT,
  -- 'voluntary' | 'involuntary' | null
  failure_reason TEXT,
  -- gateway_response for payment_failed events
  occurred_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sub_events_email ON subscription_events(customer_email);
CREATE INDEX idx_sub_events_type ON subscription_events(event_type, occurred_at);

Instrumenting from Webhook Events

// In your webhook handler

if (event.event === 'subscription.create') {
  await db.subscriptionEvents.create({
    subscription_code: event.data.subscription_code,
    customer_email: event.data.customer.email,
    plan_code: event.data.plan.plan_code,
    plan_name: event.data.plan.name,
    plan_amount: event.data.plan.amount / 100,
    event_type: 'created',
    occurred_at: event.data.created_at,
  });
  // Increment MRR
  await db.mrr.increment(event.data.plan.amount / 100);
}

if (event.event === 'charge.success' && event.data.plan) {
  await db.subscriptionEvents.create({
    subscription_code: event.data.subscription_code,
    customer_email: event.data.customer.email,
    plan_amount: event.data.amount / 100,
    event_type: 'renewed',
    occurred_at: event.data.paid_at,
  });
}

if (event.event === 'invoice.payment_failed') {
  await db.subscriptionEvents.create({
    subscription_code: event.data.subscription.subscription_code,
    customer_email: event.data.customer.email,
    plan_code: event.data.subscription.plan.plan_code,
    plan_amount: event.data.subscription.plan.amount / 100,
    event_type: 'payment_failed',
    churn_type: 'involuntary',
    failure_reason: event.data.transaction?.gateway_response,
    occurred_at: event.data.created_at,
  });
  // Start dunning — send card update email immediately
  await sendCardUpdateEmail(event.data.customer.email);
}

if (event.event === 'subscription.disable') {
  await db.subscriptionEvents.create({
    subscription_code: event.data.subscription_code,
    customer_email: event.data.customer.email,
    plan_amount: event.data.plan.amount / 100,
    event_type: 'cancelled',
    churn_type: 'voluntary',
    occurred_at: event.data.created_at,
  });
  // Decrement MRR
  await db.mrr.decrement(event.data.plan.amount / 100);
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Calculating Churn Rate and MRR

-- Monthly churn rate
WITH month_start AS (
  SELECT COUNT(*) as active_start
  FROM subscriptions
  WHERE status = 'active' AND created_at < '2026-07-01'
),
churned AS (
  SELECT COUNT(*) as churned_count
  FROM subscription_events
  WHERE event_type = 'cancelled'
  AND occurred_at BETWEEN '2026-07-01' AND '2026-07-31'
)
SELECT
  churned.churned_count::float / NULLIF(month_start.active_start, 0) * 100 AS churn_rate_pct
FROM month_start, churned;

-- Involuntary vs voluntary churn breakdown
SELECT
  churn_type,
  COUNT(*) as count,
  SUM(plan_amount) as mrr_lost
FROM subscription_events
WHERE event_type = 'cancelled'
  AND occurred_at BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY churn_type;

-- Top failure reasons for involuntary churn
SELECT
  failure_reason,
  COUNT(*) as occurrences
FROM subscription_events
WHERE event_type = 'payment_failed'
GROUP BY failure_reason
ORDER BY occurrences DESC
LIMIT 10;

Learn More

This guide is part of the Paystack recurring billing series.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Separate voluntary churn (subscription.disable) from involuntary churn (invoice.payment_failed). They need different responses.
  • ✓Involuntary churn from declined cards is recoverable — 30-40% of failed charges succeed on retry within 7 days.
  • ✓Track MRR by summing active subscription plan amounts. Decrement on subscription.disable, increment on new subscription.create.
  • ✓Build a subscription_events table to capture every status change with timestamp and reason for longitudinal analysis.
  • ✓Send a card update email immediately after invoice.payment_failed. Every day of delay reduces recovery rate.
  • ✓Segment churn by card type, billing cycle, and plan tier to identify which customers churn most.

Frequently Asked Questions

What is a healthy subscription churn rate for an African SaaS?
Monthly churn rates under 2% are generally healthy for B2B SaaS. For B2C, under 5% monthly is acceptable. African markets have higher involuntary churn due to card expiry and insufficient balance — which is why dunning sequences are especially important. Track voluntary and involuntary churn separately.
How do I recover a customer whose subscription was cancelled due to failed payment?
Send an email immediately with a link to update their card. If they do not respond in 3 days, send an SMS. After 7 days, offer a one-time discount or a grace extension. Customers who cancelled due to failed payment (involuntary) are much more likely to reactivate than those who actively cancelled.
Should I store MRR in the database or calculate it on the fly?
Calculate on the fly from your subscriptions table for accuracy. Storing MRR as a running counter risks drift when events are missed or replayed. A simple query summing active subscription plan amounts gives you real-time MRR without synchronization issues.
How do I track expansion MRR (upgrades) and contraction MRR (downgrades) with Paystack?
Paystack does not have native plan upgrade/downgrade events. You need to model this yourself: cancel the current subscription (subscription.disable) and create a new one at the higher or lower plan. Store both events with the old and new plan amounts to calculate expansion/contraction MRR in your analytics.

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

Handling Failed Recurring Charges and Dunning on Paystack

Recurring charges fail. Cards expire, accounts run dry, banks decline for opaque reasons. This guide covers every failure scenario in Paystack recurring billing, shows you how to read gateway responses, build a retry strategy, implement a dunning state machine, and handle the higher failure rates typical in African card markets.

Read more

Reusable vs Non-Reusable Paystack Authorizations

Not every Paystack authorization can be charged again. Card payments produce reusable authorizations that work for recurring billing. Bank transfers and USSD produce non-reusable ones. This guide explains the difference, shows you how to check and handle each type, and covers the edge cases across different African payment channels.

Read more

Coupons and Discounts on Paystack Subscriptions

Paystack does not have a native coupon system. To offer discounts on subscriptions, you either create separate discounted plans or manage coupons in your application and charge discounted amounts via charge_authorization. This guide covers both approaches with code examples, coupon validation, and tracking.

Read more

Paystack Payment Channels: Card, Bank, USSD, Mobile Money, QR

Paystack supports six payment channels: card, bank transfer, USSD, mobile money, QR, and Apple Pay. Each channel has different availability by market, different settlement behaviors, and different user experiences. This guide covers all of them so you know what your customers can use and what your code needs to handle.

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.