McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build Paystack Subscriptions in React
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Build Paystack Subscriptions in React

To build Paystack subscriptions in React, use the Paystack Inline JS popup on the frontend to collect the first payment with a plan code, and handle all server-side logic (plan creation, webhook processing, subscription management) through your backend API. The React frontend handles UI state and triggers the checkout, while your backend verifies signatures, processes lifecycle events, and manages the subscription database.

In this article

  1. 1. Architecture: React Frontend + Backend API
  2. 2. Setting Up Paystack Popup for Subscriptions
  3. 3. Building the Pricing Page
  4. 4. Backend Webhook Handling
  5. 5. Subscription Management UI in React
  6. 6. Handling Failed Payments in the UI
  7. 7. Using Redirect Checkout Instead of Popup
  8. 8. Testing and Going Live

Architecture: React Frontend + Backend API

React runs in the browser. Your Paystack secret key must never appear in browser code. This means every Paystack subscription flow splits into two parts:

  • React (frontend): Renders the pricing page, triggers the Paystack Popup for checkout, shows subscription status, and provides cancel/reactivate buttons.
  • Backend API (any server): Creates plans, initializes transactions, verifies payments, handles webhooks, manages subscription state, and processes dunning.

This guide shows the React side in detail. For the backend, the code examples use a generic HTTP API. You can implement that backend in Express, Django, Laravel, or any other server framework.

The subscription flow from the customer perspective:

  1. Customer selects a plan on your React pricing page.
  2. React opens the Paystack Popup with the plan code (or redirects to a Paystack checkout URL).
  3. Customer pays. The popup closes with a success callback.
  4. Your backend receives a subscription.create webhook from Paystack.
  5. On each renewal cycle, Paystack charges the saved card and sends charge.success to your webhook.
  6. If a charge fails, Paystack sends invoice.payment_failed, and your backend starts dunning.

Setting Up Paystack Popup for Subscriptions

The Paystack Inline JS popup is the fastest way to collect the first subscription payment. Load the script and configure it with the plan code.

First, add the Paystack script to your index.html or load it dynamically:

<script src="https://js.paystack.co/v2/inline.js"></script>

Create a reusable hook for the Paystack popup:

// hooks/usePaystackSubscription.ts
import { useCallback } from "react";

declare global {
  interface Window {
    PaystackPop: any;
  }
}

interface SubscriptionConfig {
  email: string;
  planCode: string;
  amount: number; // in kobo, must match the plan amount
  onSuccess: (reference: string) => void;
  onClose: () => void;
}

export function usePaystackSubscription() {
  const subscribe = useCallback((config: SubscriptionConfig) => {
    const popup = new window.PaystackPop();
    popup.checkout({
      key: process.env.REACT_APP_PAYSTACK_PUBLIC_KEY,
      email: config.email,
      amount: config.amount,
      plan: config.planCode,
      onSuccess: (transaction: any) => {
        config.onSuccess(transaction.reference);
      },
      onClose: () => {
        config.onClose();
      },
    });
  }, []);

  return { subscribe };
}

The plan parameter is the key difference from a one-time payment. When you include it, Paystack treats the transaction as a subscription enrollment. The amount must match the plan amount exactly, or Paystack will reject it.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Building the Pricing Page

Your pricing page shows the available plans and lets customers subscribe. Fetch plans from your backend (which calls the Paystack API) and display them.

// components/PricingPage.tsx
import { useState, useEffect } from "react";
import { usePaystackSubscription } from "../hooks/usePaystackSubscription";

interface Plan {
  plan_code: string;
  name: string;
  amount: number;
  interval: string;
  currency: string;
}

export function PricingPage({ userEmail }: { userEmail: string }) {
  const [plans, setPlans] = useState<Plan[]>([]);
  const [loading, setLoading] = useState(false);
  const { subscribe } = usePaystackSubscription();

  useEffect(() => {
    fetch("/api/plans")
      .then((res) => res.json())
      .then((data) => setPlans(data.plans));
  }, []);

  function handleSubscribe(plan: Plan) {
    setLoading(true);
    subscribe({
      email: userEmail,
      planCode: plan.plan_code,
      amount: plan.amount,
      onSuccess: (reference) => {
        // Verify the transaction on your backend
        fetch("/api/verify-subscription", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ reference: reference }),
        }).then(() => {
          setLoading(false);
          // Navigate to dashboard or show success
          window.location.href = "/dashboard";
        });
      },
      onClose: () => {
        setLoading(false);
      },
    });
  }

  function formatAmount(amount: number, currency: string) {
    return currency + " " + (amount / 100).toLocaleString();
  }

  return (
    <div>
      <h1>Choose Your Plan</h1>
      {plans.map((plan) => (
        <div key={plan.plan_code}>
          <h2>{plan.name}</h2>
          <p>{formatAmount(plan.amount, plan.currency)} / {plan.interval}</p>
          <button onClick={() => handleSubscribe(plan)} disabled={loading}>
            Subscribe
          </button>
        </div>
      ))}
    </div>
  );
}

After the popup reports success, the onSuccess callback fires with the transaction reference. Send that reference to your backend for verification. Do not grant access based solely on the popup callback. The webhook is the authoritative signal.

Backend Webhook Handling

Your backend receives all subscription lifecycle events from Paystack. The React frontend never sees these directly. Here is what each event means and what your backend should do:

subscription.create: A new subscription was created. Store the subscription code, plan code, customer code, and email token. The email token is critical for cancellation.

charge.success (with plan data): A renewal charge succeeded. Extend the customer access period. Update the subscription's current_period_end timestamp.

invoice.payment_failed: The renewal charge failed. Start your dunning sequence. Record the failure timestamp so you know when to escalate.

subscription.not_renew: The customer or your code cancelled the subscription. They still have access until the end of the current billing period. Set the status to "non_renewing."

subscription.disable: The subscription is fully stopped. Revoke access.

Your backend webhook handler (in pseudocode for any framework):

// POST /api/paystack/webhook
// 1. Read the raw request body
// 2. Verify the HMAC SHA-512 signature using your secret key
// 3. Parse the JSON
// 4. Check for idempotency (skip if you already processed this event reference)
// 5. Switch on event.event
// 6. Return 200 immediately

The React app polls your backend for subscription status changes, or you can use WebSockets or server-sent events if you want real-time updates in the UI.

Subscription Management UI in React

Once a customer is subscribed, they need to see their subscription status, current plan, next billing date, and a button to cancel.

// components/SubscriptionManager.tsx
import { useState, useEffect } from "react";

interface Subscription {
  id: string;
  planName: string;
  status: string;
  currentPeriodEnd: string;
  amount: number;
  currency: string;
  interval: string;
}

export function SubscriptionManager() {
  const [subscription, setSubscription] = useState<Subscription | null>(null);
  const [cancelling, setCancelling] = useState(false);

  useEffect(() => {
    fetch("/api/my-subscription")
      .then((res) => res.json())
      .then((data) => setSubscription(data.subscription));
  }, []);

  async function handleCancel() {
    if (!window.confirm("Cancel your subscription? You will keep access until the end of your current period.")) {
      return;
    }
    setCancelling(true);
    const res = await fetch("/api/subscription/cancel", {
      method: "POST",
    });
    if (res.ok) {
      setSubscription((prev) =>
        prev ? { ...prev, status: "non_renewing" } : null
      );
    }
    setCancelling(false);
  }

  async function handleReactivate() {
    const res = await fetch("/api/subscription/reactivate", {
      method: "POST",
    });
    if (res.ok) {
      setSubscription((prev) =>
        prev ? { ...prev, status: "active" } : null
      );
    }
  }

  if (!subscription) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h2>Your Subscription</h2>
      <p>Plan: {subscription.planName}</p>
      <p>Status: {subscription.status}</p>
      <p>Next billing: {new Date(subscription.currentPeriodEnd).toLocaleDateString()}</p>

      {subscription.status === "active" && (
        <button onClick={handleCancel} disabled={cancelling}>
          Cancel Subscription
        </button>
      )}

      {subscription.status === "non_renewing" && (
        <div>
          <p>Your subscription will end on {new Date(subscription.currentPeriodEnd).toLocaleDateString()}</p>
          <button onClick={handleReactivate}>Reactivate</button>
        </div>
      )}

      {subscription.status === "attention" && (
        <div>
          <p>Your last payment failed. Please update your card.</p>
          <a href="/update-card">Update Payment Method</a>
        </div>
      )}
    </div>
  );
}

The /api/my-subscription endpoint returns the current user's subscription data from your database. The cancel and reactivate endpoints call the Paystack API on the backend using the stored subscription code and email token.

Handling Failed Payments in the UI

When a subscription charge fails, your backend receives the invoice.payment_failed webhook and updates the subscription status to "attention" in your database. The React app should detect this state and prompt the customer to take action.

// components/PaymentFailedBanner.tsx
import { useState, useEffect } from "react";

export function PaymentFailedBanner() {
  const [manageLink, setManageLink] = useState("");
  const [show, setShow] = useState(false);

  useEffect(() => {
    fetch("/api/my-subscription")
      .then((res) => res.json())
      .then((data) => {
        if (data.subscription && data.subscription.status === "attention") {
          setShow(true);
          // Fetch the Paystack manage link from your backend
          fetch("/api/subscription/manage-link")
            .then((res) => res.json())
            .then((linkData) => setManageLink(linkData.link));
        }
      });
  }, []);

  if (!show) return null;

  return (
    <div style={{ background: "#FEE2E2", padding: "16px", borderRadius: "8px" }}>
      <p>Your last payment could not be processed. Please update your payment method to keep your subscription active.</p>
      {manageLink && (
        <a href={manageLink} target="_blank" rel="noopener noreferrer">
          Update Payment Method
        </a>
      )}
    </div>
  );
}

Place this banner component at the top of your app layout so the customer sees it on every page. The manage link opens a Paystack-hosted page where the customer can enter new card details. You never handle card numbers.

When the customer updates their card and the next charge succeeds, your backend receives a charge.success webhook, clears the dunning state, and the React app hides the banner on the next data fetch.

Using Redirect Checkout Instead of Popup

If you prefer not to use the Paystack Popup (maybe for mobile browsers where popups are unreliable), you can use redirect checkout instead. Your backend initializes the transaction and returns the authorization URL, and React redirects the browser there.

// components/RedirectCheckout.tsx
import { useState } from "react";

export function RedirectCheckout({ planCode, email }: { planCode: string; email: string }) {
  const [loading, setLoading] = useState(false);

  async function handleSubscribe() {
    setLoading(true);
    const res = await fetch("/api/subscribe", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: email,
        planCode: planCode,
      }),
    });
    const data = await res.json();

    if (data.authorization_url) {
      window.location.href = data.authorization_url;
    } else {
      setLoading(false);
      alert("Something went wrong. Please try again.");
    }
  }

  return (
    <button onClick={handleSubscribe} disabled={loading}>
      {loading ? "Redirecting..." : "Subscribe Now"}
    </button>
  );
}

The redirect approach is more reliable on mobile and works well when you want full-page checkout. The trade-off is that the customer leaves your site temporarily. After payment, Paystack redirects back to your callback URL with the transaction reference.

Testing and Going Live

During development, use Paystack test mode. Test card 4084 0840 8408 4081 with any future expiry and CVV works for successful payments.

Test the full subscription lifecycle:

  1. Subscribe using the popup or redirect flow.
  2. Verify the subscription.create webhook fires and your database updates.
  3. In the Paystack Dashboard, find the test subscription and trigger a manual renewal.
  4. Verify the charge.success webhook fires.
  5. Cancel the subscription from your React UI. Verify subscription.not_renew fires.
  6. Reactivate and verify it works.

For testing failed payments, you can create a plan with a daily interval and use a card that will fail on the second charge (check the Paystack documentation for failure simulation cards).

When switching to production:

  • Replace test keys with live keys in your environment variables.
  • Update the webhook URL in the Paystack Dashboard to your production server.
  • Verify the popup loads correctly with the live public key.
  • Send a test webhook from the Dashboard to confirm your production webhook handler processes it.

Key Takeaways

  • ✓React handles the frontend subscription UI and Paystack Popup checkout. All secret-key operations happen on your backend.
  • ✓Initialize the Paystack Popup with a plan code to automatically create a subscription after the first successful charge.
  • ✓Your backend must handle webhooks for subscription.create, charge.success, invoice.payment_failed, and subscription.disable events.
  • ✓Never call the Paystack API with your secret key from React. Always proxy through your backend.
  • ✓Store the email_token from the subscription.create webhook to enable programmatic cancellation and reactivation.
  • ✓Build dunning by listening for invoice.payment_failed and sending customers a Paystack manage link to update their card.

Frequently Asked Questions

Can I use react-paystack npm package for subscriptions?
The react-paystack package wraps the Paystack Inline JS popup and supports the plan parameter. You can use it for subscription checkout. However, make sure you pass the plan code in the configuration. The subscription creation, webhooks, and lifecycle management still happen entirely on your backend.
How do I show the subscription status in real time after payment?
The Paystack Popup onSuccess callback fires immediately after payment, but the subscription.create webhook may arrive a few seconds later. After the popup closes, either poll your backend for the subscription status, or show an optimistic "Processing your subscription" message and redirect to a page that checks the status via your backend API.
Can I change the subscription plan from the React UI?
Paystack does not support in-place plan changes. To upgrade or downgrade, cancel the current subscription from your backend and create a new one on the new plan. In your React UI, guide the customer through this: show the new plan, cancel the old subscription behind the scenes, and start a new checkout for the new plan.
Should I verify the transaction on the frontend or the backend after popup success?
Always verify on the backend. The popup callback tells you the transaction reference, but it runs in the browser where it can be tampered with. Send the reference to your backend, which calls Paystack verify endpoint with the secret key, confirms the amount and status, and then grants access.
How do I handle the case where the popup closes but the webhook has not arrived yet?
This is the webhook-redirect race condition. When the popup closes with success, show the customer a confirmation page. Your backend should be designed so that either the webhook or the verification call (whichever arrives first) creates the subscription record. Use the transaction reference as the idempotency key to prevent double-creation.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Verify Paystack Payments in Svelte and SvelteKit

Verify Paystack transactions in SvelteKit using server endpoints for popup verification and load functions for redirect callback verification. Covers amount validation, idempotent fulfillment, and SSR verification for instant results.

Read more

Handle Paystack Webhooks in Flask

Receive and verify Paystack webhooks in your Flask application. This guide covers accessing request.data for raw bytes, HMAC SHA-512 signature verification, processing charge.success events, and making handlers idempotent.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Handling Slow Networks in Paystack Checkout Flows

How to handle slow, unstable, and intermittent networks in Paystack checkout flows. Covers timeout configuration, meaningful loading states, smart retry logic, and the specific network conditions you face building for African markets.

Read more

Paystack Sandbox Card Numbers Not Working

Paystack sandbox card numbers fail for three reasons: you are using Stripe test cards instead of Paystack ones, you forgot the PIN and OTP steps, or your integration is hitting the live API with test keys. Here is how to fix each one.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.