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. Gym and Class Membership Billing with Paystack
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Gym and Class Membership Billing with Paystack

Use the Paystack Subscriptions API for standard monthly gym memberships. Implement freeze and resume in your application (Paystack has no native pause). For class packs and drop-in sessions, use one-time charges or charge_authorization. Tie billing status to your access control system so expired memberships automatically restrict entry.

In this article

  1. 1. Mapping Membership Types to Paystack
  2. 2. Setting Up Monthly Memberships
  3. 3. Implementing Membership Freeze
  4. 4. Class Pack Billing
  5. 5. Tying Billing to Access Control
  6. 6. Family Plans

Mapping Membership Types to Paystack

Gym businesses typically offer several membership types, each mapping differently to Paystack:

Membership TypePaystack Approach
Monthly unlimitedSubscriptions API (fixed amount, monthly interval)
Annual membershipSubscriptions API (fixed amount, annual interval)
Class pack (10 classes)One-time checkout charge. Track remaining classes in your database.
Drop-in / day passOne-time checkout charge per visit.
Family planSubscriptions API at the family rate. Track individual family members.
Corporate planSubscriptions API or manual invoice for the company.

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

Setting Up Monthly Memberships

var MEMBERSHIP_PLANS = {
  basic: { name: 'Basic Gym Access', planCode: 'PLN_gym_basic_xxx', amount: 1500000 },
  premium: { name: 'Premium (Gym + Classes)', planCode: 'PLN_gym_premium_xxx', amount: 2500000 },
  vip: { name: 'VIP (All Access + PT)', planCode: 'PLN_gym_vip_xxx', amount: 5000000 },
};

async function subscribeToMembership(memberEmail, membershipType) {
  var plan = MEMBERSHIP_PLANS[membershipType];

  var response = await axios.post(
    'https://api.paystack.co/transaction/initialize',
    {
      email: memberEmail,
      amount: plan.amount,
      plan: plan.planCode,
      callback_url: 'https://yourgym.com/membership/callback',
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data.data.authorization_url;
}

When the payment completes, the webhook creates the subscription and you activate the membership in your system. Link the billing status to the member's profile so your front-desk software and access control system know whether to grant entry.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Implementing Membership Freeze

var FREEZE_POLICY = {
  maxFreezeDays: 30,
  maxFreezesPerYear: 2,
  minimumActiveBeforeFreeze: 30,
};

async function freezeMembership(userId, freezeDays, reason) {
  if (freezeDays > FREEZE_POLICY.maxFreezeDays) {
    return { success: false, message: 'Maximum freeze duration is ' + FREEZE_POLICY.maxFreezeDays + ' days.' };
  }

  var billing = await getCurrentBilling(userId);

  // Cancel on Paystack
  await disableSubscription(billing.paystack_subscription_code, billing.paystack_email_token);

  var resumeDate = new Date();
  resumeDate.setDate(resumeDate.getDate() + freezeDays);

  await db.query(
    'UPDATE memberships SET status = $1, frozen_at = NOW(), resume_date = $2, freeze_reason = $3, updated_at = NOW() WHERE user_id = $4',
    ['frozen', resumeDate, reason, userId]
  );

  // Update access control
  await updateAccessControl(userId, 'deny');

  return {
    success: true,
    frozenUntil: resumeDate,
    message: 'Membership frozen until ' + formatDate(resumeDate) + '.',
  };
}

// Daily cron: auto-resume frozen memberships
async function autoResumeFrozenMemberships() {
  var toResume = await db.query(
    'SELECT m.user_id, m.membership_type, u.email FROM memberships m JOIN users u ON m.user_id = u.id WHERE m.status = $1 AND m.resume_date <= CURRENT_DATE',
    ['frozen']
  );

  for (var i = 0; i < toResume.rows.length; i++) {
    var member = toResume.rows[i];
    var paymentMethod = await getDefaultPaymentMethod(member.user_id);

    if (!paymentMethod) {
      await sendResumeFailedEmail(member.email, 'No card on file');
      continue;
    }

    var plan = MEMBERSHIP_PLANS[member.membership_type];
    var authCode = await getAuthorizationCode(paymentMethod.id);

    try {
      var newSub = await createPaystackSubscription(member.email, plan.planCode, authCode);

      await db.query(
        'UPDATE memberships SET status = $1, frozen_at = NULL, resume_date = NULL, updated_at = NOW() WHERE user_id = $2',
        ['active', member.user_id]
      );

      await updateAccessControl(member.user_id, 'allow');
      await sendMembershipResumedEmail(member.email);
    } catch (error) {
      console.log('Resume failed for ' + member.user_id + ': ' + error.message);
    }
  }
}

Class Pack Billing

var CLASS_PACKS = {
  pack_5: { name: '5 Class Pack', classes: 5, amount: 500000 },
  pack_10: { name: '10 Class Pack', classes: 10, amount: 900000 },
  pack_20: { name: '20 Class Pack', classes: 20, amount: 1600000 },
};

async function purchaseClassPack(userId, packType) {
  var pack = CLASS_PACKS[packType];
  var user = await getUser(userId);

  var response = await axios.post(
    'https://api.paystack.co/transaction/initialize',
    {
      email: user.email,
      amount: pack.amount,
      callback_url: 'https://yourgym.com/classes/pack-purchased',
      metadata: { pack_type: packType, user_id: userId },
    },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
    }
  );

  return response.data.data.authorization_url;
}

// After successful payment
async function activateClassPack(userId, packType) {
  var pack = CLASS_PACKS[packType];

  await db.query(
    'INSERT INTO class_packs (user_id, pack_type, total_classes, remaining_classes, purchased_at, expires_at) VALUES ($1, $2, $3, $4, NOW(), NOW() + INTERVAL '90 days')',
    [userId, packType, pack.classes, pack.classes]
  );
}

// When member checks into a class
async function deductClassFromPack(userId) {
  var activePack = await db.query(
    'SELECT id, remaining_classes FROM class_packs WHERE user_id = $1 AND remaining_classes > 0 AND expires_at > NOW() ORDER BY purchased_at ASC LIMIT 1',
    [userId]
  );

  if (activePack.rows.length === 0) {
    return { success: false, message: 'No active class pack. Please purchase one.' };
  }

  await db.query(
    'UPDATE class_packs SET remaining_classes = remaining_classes - 1 WHERE id = $1',
    [activePack.rows[0].id]
  );

  return {
    success: true,
    remaining: activePack.rows[0].remaining_classes - 1,
  };
}

Tying Billing to Access Control

// API for your access control system (door scanner, front desk app)
router.get('/api/access/check/:memberId', async function(req, res) {
  var userId = req.params.memberId;

  var membership = await db.query(
    'SELECT status FROM memberships WHERE user_id = $1',
    [userId]
  );

  if (membership.rows.length === 0) {
    return res.json({ access: false, reason: 'No membership found' });
  }

  var status = membership.rows[0].status;

  if (status === 'active') {
    return res.json({ access: true });
  }

  if (status === 'frozen') {
    return res.json({ access: false, reason: 'Membership is frozen' });
  }

  if (status === 'past_due') {
    // Grace period: allow access for 3 days after payment failure
    var billing = await getCurrentBilling(userId);
    var daysSinceFailure = Math.floor(
      (Date.now() - new Date(billing.status_changed_at).getTime()) / (24 * 60 * 60 * 1000)
    );

    if (daysSinceFailure <= 3) {
      return res.json({ access: true, warning: 'Payment overdue. Please update your card.' });
    }
    return res.json({ access: false, reason: 'Payment required' });
  }

  res.json({ access: false, reason: 'Membership is ' + status });
});

Real-time access checks ensure that billing status directly controls physical access. When a membership payment fails, the member can still enter during the grace period but is prompted to update their payment. After the grace period, the door scanner denies entry.

Family Plans

Family plans charge one card for multiple members. One Paystack subscription covers the whole family.

async function createFamilyPlan(primaryMemberEmail, familyMembers) {
  // Subscribe the primary member to the family plan
  var familyPlanCode = 'PLN_gym_family_xxx'; // e.g., 35,000 NGN/month for up to 4 members

  var checkoutUrl = await subscribeToMembership(primaryMemberEmail, familyPlanCode);

  // Store family members linked to the primary account
  for (var i = 0; i < familyMembers.length; i++) {
    await db.query(
      'INSERT INTO family_members (primary_user_id, member_name, relationship, created_at) VALUES ((SELECT id FROM users WHERE email = $1), $2, $3, NOW())',
      [primaryMemberEmail, familyMembers[i].name, familyMembers[i].relationship]
    );
  }

  return checkoutUrl;
}

All family members get access as long as the primary member's subscription is active. When the primary member's payment fails or membership is frozen, all family members lose access too. Make this policy clear during signup.

Key Takeaways

  • ✓The Subscriptions API works well for standard monthly gym memberships with fixed fees.
  • ✓Implement membership freeze in your application by cancelling the subscription and storing the authorization for later resume.
  • ✓Class packs (10 classes, 20 classes) are one-time purchases, not subscriptions. Use standard Paystack checkout.
  • ✓Tie billing status to your access control system. When a membership expires or is frozen, entry should be denied automatically.
  • ✓Family plans charge one card for multiple members. Use one authorization code and one subscription for the family.
  • ✓The biggest billing challenge for gyms is the freeze/resume cycle. Build clear policies and communicate them well.

Frequently Asked Questions

Should I use the Subscriptions API or charge_authorization for gym memberships?
Use the Subscriptions API for standard monthly or annual memberships with fixed fees. It handles scheduling and renewals automatically. Use charge_authorization for class packs, variable-amount billing, or if you need custom retry logic.
How do I handle membership freezing if Paystack has no pause feature?
Cancel the subscription on Paystack, mark the membership as frozen in your database, and store the resume date. When the freeze period ends, create a new subscription using the stored authorization code. The billing date resets to the resume date.
What happens if a frozen member's card expires during the freeze?
When the freeze ends and you try to create a new subscription, the authorization code will fail. Notify the member that their card has expired and ask them to make a new payment to resume their membership.
How do I handle class pack expiry?
Set an expires_at date when the pack is purchased (typically 60-90 days). When checking in for a class, verify that the pack has remaining classes AND has not expired. Expired classes are forfeit according to your terms.

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

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

Building a Membership Site on Paystack

A membership site charges members a recurring fee in exchange for gated content, community access, or perks. This guide covers creating Paystack plans, implementing the subscription checkout, gating content by membership status, and handling the full subscription lifecycle in your backend.

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

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.