Bonaventure OgetoBy Bonaventure Ogeto|

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.

Sign up for the McTaba newsletter

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