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. Accept Payments with Paystack in Next.js Pages Router
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Accept Payments with Paystack in Next.js Pages Router

To accept Paystack payments in Next.js Pages Router, create an API route (pages/api/pay.ts) that initializes a transaction with the Paystack API. Return the authorization URL to your frontend, redirect the user to Paystack checkout, then verify the payment in a callback API route or using getServerSideProps before granting value.

In this article

  1. 1. Prerequisites
  2. 2. Project Setup and Environment Variables
  3. 3. Initialize a Transaction with an API Route
  4. 4. Build the Checkout Page
  5. 5. Inline Popup Checkout
  6. 6. Verify Payment on the Callback Page
  7. 7. Production Checklist
  8. 8. Ship Payments Faster

Prerequisites

Before you start, you need:

  • A Paystack account with API keys (Settings > API Keys & Webhooks)
  • Node.js 16 or later
  • A Next.js project using the Pages Router (the pages/ directory)

If your project uses the App Router (app/ directory), see Accept Payments with Paystack in Next.js App Router instead.

Project Setup and Environment Variables

Create a fresh Next.js project with the Pages Router or use your existing one:

npx create-next-app@latest paystack-pages --typescript
cd paystack-pages

Add your Paystack keys to .env.local:

PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_BASE_URL=http://localhost:3000

Variables without the NEXT_PUBLIC_ prefix are only available on the server (API routes, getServerSideProps, getStaticProps). The secret key stays server-side. The public key is needed only if you use the inline popup.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Initialize a Transaction with an API Route

Create an API route at pages/api/pay.ts:

// pages/api/pay.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { email, amount } = req.body;

  if (!email || !amount) {
    return res.status(400).json({ error: 'Email and amount are required' });
  }

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

  const paystackRes = 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,
        amount: Math.round(amount * 100),
        reference,
        callback_url: process.env.NEXT_PUBLIC_BASE_URL + '/payment/callback',
      }),
    }
  );

  const data = await paystackRes.json();

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

  // TODO: Save reference, email, and expected amount to your database

  return res.status(200).json({
    authorization_url: data.data.authorization_url,
    access_code: data.data.access_code,
    reference: data.data.reference,
  });
}

This API route receives the customer's email and amount from your frontend, calls Paystack to initialize a transaction, and returns the checkout URL. The secret key never leaves the server.

Build the Checkout Page

Create a simple checkout page at pages/checkout.tsx:

// pages/checkout.tsx
import { useState } from 'react';

export default function CheckoutPage() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const amount = 5000; // 5,000 NGN

  async function handlePay() {
    setLoading(true);

    const res = await fetch('/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, amount }),
    });

    const data = await res.json();

    if (data.authorization_url) {
      window.location.href = data.authorization_url;
    } else {
      alert(data.error || 'Payment initialization failed');
      setLoading(false);
    }
  }

  return (
    <div style={{ maxWidth: 400, margin: '80px auto' }}>
      <h1>Checkout</h1>
      <input
        type="email"
        placeholder="Your email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        style={{ width: '100%', padding: 8, marginBottom: 16 }}
      />
      <p>Amount: NGN {amount.toLocaleString()}</p>
      <button onClick={handlePay} disabled={loading || !email}>
        {loading ? 'Redirecting...' : 'Pay Now'}
      </button>
    </div>
  );
}

Clicking "Pay Now" sends the email and amount to your API route, then redirects the user to Paystack's hosted checkout. Paystack handles card entry, 3DS, bank transfers, and all other payment channel logic.

Inline Popup Checkout

If you prefer the customer stays on your page during payment, load Paystack Inline.js and use the access code from your API route:

// pages/checkout-popup.tsx
import { useState } from 'react';
import Script from 'next/script';

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

export default function PopupCheckout() {
  const [email, setEmail] = useState('');

  async function handlePay() {
    const res = await fetch('/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, amount: 5000 }),
    });

    const data = await res.json();

    if (!data.access_code) {
      alert(data.error || 'Failed to initialize');
      return;
    }

    const popup = new window.PaystackPop();
    popup.checkout({
      accessCode: data.access_code,
      onSuccess: async (transaction: { reference: string }) => {
        // Verify on your server before granting value
        const verifyRes = await fetch(
          '/api/verify?reference=' + transaction.reference
        );
        const result = await verifyRes.json();
        if (result.success) {
          window.location.href = '/payment/success';
        }
      },
      onCancel: () => {
        console.log('Customer cancelled');
      },
    });
  }

  return (
    <>
      <Script src="https://js.paystack.co/v2/inline.js" />
      <div style={{ maxWidth: 400, margin: '80px auto' }}>
        <h1>Pay with Popup</h1>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <button onClick={handlePay} disabled={!email}>Pay NGN 5,000</button>
      </div>
    </>
  );
}

The next/script component loads Paystack's JavaScript from the CDN. After successful payment in the popup, you call your verify API route before redirecting to a success page.

Verify Payment on the Callback Page

Create the verification API route at pages/api/verify.ts:

// pages/api/verify.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const reference = req.query.reference as string;

  if (!reference) {
    return res.status(400).json({ error: 'Missing reference' });
  }

  const paystackRes = await fetch(
    'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  const data = await paystackRes.json();

  if (data.status && data.data.status === 'success') {
    // TODO: Verify amount and currency match your database record
    // TODO: Mark order as paid (idempotently)

    return res.status(200).json({
      success: true,
      amount: data.data.amount / 100,
      currency: data.data.currency,
      reference: data.data.reference,
    });
  }

  return res.status(400).json({ success: false, status: data.data?.status });
}

Now create the callback page using getServerSideProps to verify before rendering:

// pages/payment/callback.tsx
import type { GetServerSideProps } from 'next';

interface Props {
  success: boolean;
  reference: string;
  amount?: number;
  currency?: string;
  error?: string;
}

export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
  const reference = (context.query.reference || context.query.trxref) as string;

  if (!reference) {
    return { props: { success: false, reference: '', error: 'No reference' } };
  }

  const res = await fetch(
    'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  const data = await res.json();

  if (data.status && data.data.status === 'success') {
    // TODO: Mark order as paid in your database

    return {
      props: {
        success: true,
        reference,
        amount: data.data.amount / 100,
        currency: data.data.currency,
      },
    };
  }

  return {
    props: {
      success: false,
      reference,
      error: 'Payment status: ' + (data.data?.status || 'unknown'),
    },
  };
};

export default function PaymentCallback({ success, reference, amount, currency, error }: Props) {
  if (success) {
    return (
      <div style={{ maxWidth: 500, margin: '80px auto', textAlign: 'center' }}>
        <h1>Payment Successful</h1>
        <p>Reference: {reference}</p>
        <p>Amount: {currency} {amount?.toLocaleString()}</p>
        <a href="/">Back to Home</a>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: 500, margin: '80px auto', textAlign: 'center' }}>
      <h1>Payment Failed</h1>
      <p>{error}</p>
      <a href="/checkout">Try Again</a>
    </div>
  );
}

Using getServerSideProps means the verification runs on the server before the page HTML is sent to the browser. The customer sees either a success page or a failure page. They never see an intermediate loading state.

Production Checklist

Before going live:

  1. Switch to live keys. Replace sk_test_ and pk_test_ with live keys in your production environment variables.
  2. Set up webhooks. Add your webhook URL in the Paystack dashboard. Webhooks handle cases where the redirect fails (customer closes browser, network drops).
  3. Verify amount and currency. Always compare the verified amount and currency against your database record. Do not just check status === 'success'.
  4. Handle double-delivery. Both webhook and callback can trigger fulfillment. Use a database flag to prevent granting value twice.
  5. Use HTTPS. Paystack requires HTTPS for callback and webhook URLs in production.

For webhook handling in Next.js Pages Router, see Handle Paystack Webhooks in Next.js Pages Router.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series. We cover webhooks, verification, subscriptions, and more for every major stack.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓API routes in pages/api/ run server-side and are the right place for Paystack secret key operations. Never import your secret key in a page component.
  • ✓Use getServerSideProps on the callback page to verify the transaction before the page renders. This keeps the verification logic on the server.
  • ✓Amounts must be in the smallest currency unit. For NGN, multiply the Naira amount by 100 to get kobo.
  • ✓The redirect callback URL receives ?reference=REF as a query parameter. Extract it and verify before granting any value.
  • ✓Set up webhooks alongside the redirect callback. Webhooks fire even if the customer closes their browser mid-payment.
  • ✓Test keys (sk_test_) and live keys (sk_live_) use separate transaction pools. A reference created with test keys cannot be verified with live keys.

Frequently Asked Questions

Should I use the Pages Router or App Router for a new Paystack integration?
If you are starting a new project, the App Router is the recommended approach from Next.js. It offers Server Actions, better streaming, and a simpler mental model for server-side operations. If you have an existing Pages Router project, there is no need to migrate just for Paystack. Both routers work fine with the Paystack API.
Can I call the Paystack API directly from getServerSideProps?
Yes. getServerSideProps runs on the server, so it is safe to use your Paystack secret key there. This is useful for the callback page where you want to verify the payment before the page renders.
Why do I get a CORS error when calling Paystack from the frontend?
You cannot call the Paystack API directly from the browser. The API requires your secret key in the Authorization header, and exposing that key in the browser is a security vulnerability. Always make Paystack API calls from your API routes (server-side) and have your frontend call your API routes instead.
How do I test Paystack payments locally?
Use your test keys (sk_test_ and pk_test_). Paystack provides test card numbers in their documentation. For testing webhooks locally, use ngrok or Cloudflare Tunnel to expose your localhost to the internet, then set that URL as your webhook URL in the Paystack dashboard.
What happens if my server crashes between payment and verification?
This is exactly why webhooks exist. Even if your callback page never loads, the webhook will fire with the payment confirmation. Set up a webhook handler that independently verifies and fulfills orders. When both webhook and callback are working, whichever fires first handles fulfillment, and the other one is a no-op.

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

Accepting QR Payments Through Paystack

QR payments let customers scan a code with their banking app and pay without entering card details, typing USSD codes, or making a manual bank transfer. This guide covers how QR payments work on Paystack, how to generate QR codes for your customers, and how to build the payment flow end-to-end.

Read more

Paystack Dedicated Account Not Assigned: Every Cause and Fix

Your Paystack dedicated virtual account request failed because a prerequisite was not met. This guide walks through every eligibility requirement and shows you exactly how to diagnose and fix the problem.

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.