Build a Gym Membership App
Build a gym membership app by creating Paystack Plans (monthly, quarterly, annual tiers), subscribing members via the transaction initialize endpoint with a plan attached, and gating check-in access on active membership status. Listen for subscription lifecycle webhooks to suspend access when payments fail and restore it when they succeed.
Data Model and Membership Tiers
Tables: plans (name, duration, price, paystack_plan_code), members (name, email, phone, joined_at), memberships (member_id, plan_id, status, starts_at, expires_at, paystack_subscription_code), checkins (member_id, checked_in_at, method).
Create Paystack Plans once per tier (do this via dashboard or API on first setup):
async function createPlan(name, amountInKobo, interval) {
var res = await fetch('https://api.paystack.co/plan', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, amount: amountInKobo, interval, currency: 'NGN' }),
});
var data = await res.json();
return data.data.plan_code; // store this in your plans table
}
// Run once:
// createPlan('Monthly Membership', 1500000, 'monthly') // NGN 15,000
// createPlan('Quarterly Membership', 4000000, 'quarterly') // NGN 40,000
When a member signs up, initialize a transaction with the plan attached. After the first payment, Paystack creates a subscription automatically and charges the card monthly.
Subscription Signup and Access Control
async function signupMember(memberId, planId) {
var member = await db.members.findById(memberId);
var plan = await db.plans.findById(planId);
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: member.email,
amount: plan.price,
plan: plan.paystack_plan_code,
metadata: { member_id: memberId, plan_id: planId },
}),
});
var data = await res.json();
return data.data.authorization_url;
}
// Webhook handler
if (event.event === 'subscription.create') {
var meta = event.data.metadata;
var expiresAt = new Date();
expiresAt.setMonth(expiresAt.getMonth() + 1);
await db.memberships.create({
member_id: meta.member_id,
plan_id: meta.plan_id,
status: 'active',
paystack_subscription_code: event.data.subscription_code,
expires_at: expiresAt,
});
}
if (event.event === 'invoice.payment_failed') {
var code = event.data.subscription.subscription_code;
// Give 3-day grace period before suspending
await db.memberships.update(code, { status: 'grace_period', grace_until: addDays(new Date(), 3) });
}
if (event.event === 'subscription.not_renew' || event.event === 'subscription.disable') {
var code = event.data.subscription_code;
await db.memberships.update(code, { status: 'suspended' });
}
// Check-in access gate
async function canCheckin(memberId) {
var membership = await db.memberships.findActive(memberId);
if (!membership) return false;
if (membership.status === 'active' || membership.status === 'grace_period') return true;
return false;
}
Learn More
See build a sports club subscription system for a similar recurring billing pattern applied to clubs and leagues.
Key Takeaways
- ✓Use Paystack Plans for monthly membership billing — recurring charges happen automatically without your intervention.
- ✓Gate check-in access on an active membership record, not on payment dates in your own logic.
- ✓Handle invoice.payment_failed with a grace period (3-5 days) before suspending access — card failures are often temporary.
- ✓Support membership freeze: pause the subscription on Paystack and extend the expiry date on your end.
- ✓Offer family plans using split subscriptions or bundle pricing with subaccounts for gym franchise splits.
Frequently Asked Questions
- How do I handle membership freeze (when a member travels for a month)?
- Call the Paystack Disable Subscription endpoint to pause billing temporarily. On your end, extend the membership expiry_at by the freeze duration. When the member returns, re-enable the subscription via Paystack. Budget for this in your revenue model — freeze periods reduce your recurring revenue but improve retention.
- Can members pay monthly via M-Pesa in Kenya?
- Paystack supports M-Pesa for one-time transactions in Kenya, but recurring M-Pesa charges (standing orders) require Safaricom Business API. For Kenyan gyms, a common approach is card-on-file for recurring billing and M-Pesa for one-time top-ups or day passes. Alternatively, use M-Pesa standing orders via Daraja directly for automated monthly collection.
- How do I issue a partial refund when a member cancels mid-month?
- Calculate the unused days as a proportion of the billing period and refund that amount using the Paystack Refund API with the transaction reference and amount. Cancel the subscription simultaneously to stop future charges. Log the cancellation reason for retention analysis.
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