Build a Podcast Membership Platform
Build a podcast membership platform by creating a Paystack Plan for your paid tier, subscribing listeners via the transaction initialize endpoint with the plan code, gating member-only episode access on active subscription status in your database, and listening to subscription.create, invoice.payment_failed, and subscription.disable webhooks to grant or revoke access automatically.
Plan Setup and Member Access
Tables: membership_plans (name, price, paystack_plan_code, features[]), members (user_id, plan_id, paystack_subscription_code, status, current_period_end), episodes (title, audio_url, is_members_only, published_at).
var PLANS = {
free: { tier: 0, label: 'Free — public episodes only' },
supporter: { tier: 1, price: 100000, label: 'Supporter — all episodes + bonus content' }, // NGN 1,000/mo
};
async function subscribeToPlan(userId, userEmail) {
var plan = await db.membershipPlans.findByName('supporter');
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: userEmail,
amount: plan.price,
plan: plan.paystack_plan_code,
metadata: { user_id: userId, plan_id: plan.id },
}),
});
return (await res.json()).data.authorization_url;
}
// Content access gate
async function canAccessEpisode(userId, episodeId) {
var episode = await db.episodes.findById(episodeId);
if (!episode.is_members_only) return true;
var member = await db.members.findActiveByUser(userId);
return !!(member && member.status === 'active');
}
async function getEpisodeAudio(userId, episodeId) {
var canAccess = await canAccessEpisode(userId, episodeId);
if (!canAccess) throw { status: 403, message: 'Subscribe to access this episode.' };
var episode = await db.episodes.findById(episodeId);
return episode.audio_url;
}
Subscription Webhooks and Member Lifecycle
if (event.event === 'subscription.create') {
var meta = event.data.metadata;
await db.members.upsert({
user_id: meta.user_id,
plan_id: meta.plan_id,
paystack_subscription_code: event.data.subscription_code,
status: 'active',
current_period_end: new Date(event.data.next_payment_date),
});
await sendWelcomeEmail(meta.user_id);
}
if (event.event === 'invoice.create' && event.data.status === 'success') {
var code = event.data.subscription.subscription_code;
await db.members.update({ code }, {
status: 'active',
current_period_end: new Date(event.data.next_payment_date),
});
}
if (event.event === 'invoice.payment_failed') {
var code = event.data.subscription.subscription_code;
await db.members.update({ code }, { status: 'grace_period' });
await sendPaymentFailedEmail(code); // ask them to update payment method
}
if (event.event === 'subscription.disable') {
var code = event.data.subscription_code;
// Keep access until current_period_end; cron downgrades after
await db.members.update({ code }, { status: 'cancelled' });
}
// Cron: revoke access when period ends for cancelled/grace members
async function revokeExpiredAccess() {
var expired = await db.members.findExpiredBefore(new Date());
for (var member of expired) {
await db.members.update(member.id, { status: 'expired' });
await sendRenewalPromptEmail(member.user_id);
}
}
Learn More
See build a streaming service billing backend for the multi-tier version of this membership model applied to video content.
Key Takeaways
- ✓Use Paystack Plans for recurring billing — one plan per membership tier.
- ✓Gate member-only episode audio URLs on active subscription status in your database.
- ✓Handle invoice.payment_failed with a grace period before revoking access.
- ✓Send a welcome email on subscription.create and a renewal reminder before the next billing date.
- ✓A free tier with public episodes is your discovery funnel — do not put everything behind the paywall.
Frequently Asked Questions
- Should I offer a free tier alongside the paid membership?
- Yes. A free tier with your back catalogue and occasional public episodes is how new listeners discover you. The paid tier unlocks new episodes early, bonus content, ad-free listening, or a private community. The free tier is a funnel, not a charity — every free listener is a potential paying member.
- How do I handle a listener who subscribes and immediately asks for a refund?
- Define a 48-hour cooling-off period with full refund via the Paystack Refund API. After 48 hours, no refunds — the subscription grants immediate content access so there is no unused period to refund. State this clearly on the subscription page before checkout.
- Can I sell lifetime memberships instead of monthly subscriptions?
- Yes. Use a one-time Paystack charge (not a Plan) for a lifetime membership. On charge.success, set the member status to "lifetime" with a far-future current_period_end (e.g., year 2099). Lifetime memberships are harder to predict for revenue but generate a cash injection and a committed core audience.
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