Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in Node.js and Express

To build Paystack subscriptions in Express, first create a plan via the Paystack API with an interval (monthly, yearly, etc.) and amount. Then initialize a transaction with the plan code. After the first payment, Paystack automatically charges the customer on the plan interval. Handle subscription.create and invoice.payment_failed webhooks to track status.

How Paystack Subscriptions Work

Paystack subscriptions have three parts:

  1. Plans. A plan defines the amount, currency, billing interval (daily, weekly, monthly, quarterly, biannually, annually), and optional invoice limit.
  2. Subscriptions. When a customer pays for a plan, Paystack creates a subscription linking the customer to the plan. Paystack stores the customer's payment authorization and uses it for future charges.
  3. Invoices. On each billing cycle, Paystack creates an invoice and attempts to charge the customer. If it succeeds, you get a charge.success webhook. If it fails, you get invoice.payment_failed.

You do not need to build a cron job or scheduler. Paystack handles all the timing and retry logic for recurring charges.

Create a Plan

Create a plan via the API or the dashboard. Via the API:

var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

app.post('/api/plans', async function(req, res) {
  var name = req.body.name;
  var amount = req.body.amount;
  var interval = req.body.interval; // monthly, yearly, weekly, etc.

  var paystackRes = await fetch('https://api.paystack.co/plan', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + PAYSTACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: name,
      amount: Math.round(amount * 100),
      interval: interval,
      currency: 'NGN',
    }),
  });

  var data = await paystackRes.json();

  if (!data.status) {
    return res.status(400).json({ error: data.message });
  }

  // Save the plan_code to your database
  var planCode = data.data.plan_code;

  res.json({
    plan_code: planCode,
    name: data.data.name,
    amount: data.data.amount / 100,
    interval: data.data.interval,
  });
});

You only create each plan once. Store the plan_code in your database and reuse it for all customers subscribing to that plan. Most teams create plans through the dashboard and hardcode the plan codes in their application config.

Subscribe a Customer

Initialize a transaction with the plan code. The first payment creates the subscription:

app.post('/api/subscribe', async function(req, res) {
  var email = req.body.email;
  var planCode = req.body.plan_code;

  var reference = 'sub_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);

  var paystackRes = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + PAYSTACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      plan: planCode,
      reference: reference,
      callback_url: process.env.BASE_URL + '/subscription/callback',
    }),
  });

  var data = await paystackRes.json();

  if (!data.status) {
    return res.status(400).json({ error: data.message });
  }

  // Save reference and plan to your database
  res.json({
    authorization_url: data.data.authorization_url,
    reference: data.data.reference,
  });
});

When you pass a plan parameter, you do not need to pass amount. Paystack uses the plan's amount. After the customer pays, Paystack creates the subscription and stores the payment authorization for future charges.

Handle Subscription Webhooks

Subscription management depends heavily on webhooks. Here are the events you need to handle:

async function handlePaystackEvent(event) {
  var eventType = event.event;
  var data = event.data;

  if (eventType === 'subscription.create') {
    // New subscription created
    await db.subscriptions.insertOne({
      subscriptionCode: data.subscription_code,
      customerEmail: data.customer.email,
      customerCode: data.customer.customer_code,
      planCode: data.plan.plan_code,
      status: 'active',
      nextPaymentDate: data.next_payment_date,
      createdAt: new Date(),
    });

  } else if (eventType === 'charge.success' && data.plan) {
    // Subscription renewal payment succeeded
    await db.subscriptions.updateOne(
      { subscriptionCode: data.subscription_code },
      { $set: { status: 'active', lastPaymentDate: new Date() } }
    );

  } else if (eventType === 'invoice.payment_failed') {
    // Subscription renewal failed
    await db.subscriptions.updateOne(
      { subscriptionCode: data.subscription.subscription_code },
      { $set: { status: 'past_due' } }
    );
    // Notify the customer to update their payment method

  } else if (eventType === 'subscription.disable') {
    // Subscription cancelled
    await db.subscriptions.updateOne(
      { subscriptionCode: data.subscription_code },
      { $set: { status: 'cancelled', cancelledAt: new Date() } }
    );
  }
}

Track the subscription status in your database. Use it to gate access to premium features. When a renewal fails, give the customer a grace period to update their payment method before revoking access.

Cancel and Manage Subscriptions

Cancel a subscription via the API:

app.post('/api/subscriptions/cancel', async function(req, res) {
  var subscriptionCode = req.body.subscription_code;
  var emailToken = req.body.email_token;

  var paystackRes = await fetch('https://api.paystack.co/subscription/disable', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + PAYSTACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      code: subscriptionCode,
      token: emailToken,
    }),
  });

  var data = await paystackRes.json();

  if (!data.status) {
    return res.status(400).json({ error: data.message });
  }

  res.json({ message: 'Subscription cancelled' });
});

The email_token is sent in the subscription.create webhook event. Store it alongside the subscription code. You need it for cancellation.

Alternatively, Paystack provides a cancellation link for each subscription. You can redirect customers to that link instead of building your own cancellation flow.

Check Subscription Status

Fetch the current status of a subscription from Paystack:

app.get('/api/subscriptions/:code', async function(req, res) {
  var code = req.params.code;

  var paystackRes = await fetch(
    'https://api.paystack.co/subscription/' + encodeURIComponent(code),
    { headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET } }
  );

  var data = await paystackRes.json();

  if (!data.status) {
    return res.status(400).json({ error: data.message });
  }

  res.json({
    status: data.data.status,
    plan: data.data.plan.name,
    next_payment_date: data.data.next_payment_date,
    email_token: data.data.email_token,
  });
});

Use this to display the customer's subscription details in your app. For frequent access checks, rely on the status stored in your database (updated by webhooks) rather than calling Paystack on every page load.

Ship Payments Faster

Key Takeaways

  • Paystack handles all the recurring billing logic. You create a plan, subscribe a customer, and Paystack charges them automatically on the interval.
  • Plans are created once and reused. Each plan has a plan_code you pass when initializing a subscription transaction.
  • The first payment creates the subscription. Subsequent charges happen automatically without customer interaction.
  • Webhook events are critical for subscriptions. Handle subscription.create, charge.success, invoice.payment_failed, and subscription.disable.
  • Customers can cancel via a link Paystack provides, or you can cancel programmatically via the API.
  • Store the subscription_code and customer_code in your database. You need them to manage, pause, or cancel subscriptions.

Frequently Asked Questions

Can I change the price of a plan after customers have subscribed?
Updating a plan amount only affects new subscribers. Existing subscribers continue paying the original amount. To change the price for existing subscribers, create a new plan, cancel their current subscription, and subscribe them to the new plan.
What happens when a subscription payment fails?
Paystack retries the charge according to its retry schedule. You receive an invoice.payment_failed webhook for each failed attempt. After exhausting retries, the subscription status changes. Handle this by notifying the customer and providing a way to update their payment method.
Can customers upgrade or downgrade their plan?
Paystack does not have a built-in plan-change feature. To switch plans, cancel the current subscription and create a new one on the desired plan. Handle prorating on your side if needed.
How do I offer a free trial before charging?
Paystack does not have a native free trial feature. Implement trials in your application: grant access for the trial period, then initialize the subscription transaction when the trial ends. Collect payment details at trial start if you want a seamless transition.

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