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.
Gym businesses typically offer several membership types, each mapping differently to Paystack:
| Membership Type | Paystack Approach |
|---|---|
| Monthly unlimited | Subscriptions API (fixed amount, monthly interval) |
| Annual membership | Subscriptions API (fixed amount, annual interval) |
| Class pack (10 classes) | One-time checkout charge. Track remaining classes in your database. |
| Drop-in / day pass | One-time checkout charge per visit. |
| Family plan | Subscriptions API at the family rate. Track individual family members. |
| Corporate plan | Subscriptions API or manual invoice for the company. |
This article is part of the subscriptions and recurring billing guide.
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.
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);
}
}
}
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,
};
}
// 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 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.
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