In a headless CMS setup: 1) Fetch product data (price, name) from your CMS API (Sanity GROQ or Contentful REST/GraphQL). 2) Create a Next.js API route at /api/checkout that calls POST /transaction/initialize with the Paystack secret key server-side. 3) Redirect the customer to the Paystack authorization_url. 4) Handle charge.success webhook in /api/webhooks/paystack — verify HMAC and update your order DB.
The flow: Next.js frontend fetches product data from CMS → customer clicks Buy → Next.js API route initializes Paystack with price from CMS → customer is redirected to Paystack checkout → on success, Paystack fires a webhook to your Next.js API route → you update your database and fulfill the order.
Key rule: always fetch the price from your CMS server-side before passing to Paystack. Never pass a client-supplied price to Paystack — a customer could modify the request to pay less.
// pages/api/checkout.js (or app/api/checkout/route.js in App Router)
import { createClient } from '@sanity/client';
var sanity = createClient({ projectId: process.env.SANITY_PROJECT_ID, dataset: 'production', useCdn: true, apiVersion: '2024-01-01' });
export default async function handler(req, res) {
if (req.method !== 'POST') return res.status(405).end();
var { productId, email } = req.body;
// Fetch price from CMS — never trust client-supplied price
var product = await sanity.fetch('*[_id == $id][0]{ name, price }', { id: productId });
if (!product) return res.status(404).json({ error: 'Product not found' });
var reference = 'order_' + productId + '_' + Date.now();
var response = 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(product.price * 100), // NGN to kobo
reference,
metadata: { productId, productName: product.name },
callback_url: process.env.NEXT_PUBLIC_BASE_URL + '/checkout/callback',
}),
});
var data = await response.json();
res.json({ url: data.data.authorization_url });
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// pages/api/webhooks/paystack.js
import crypto from 'crypto';
export const config = { api: { bodyParser: false } };
async function getRawBody(req) {
return new Promise((resolve, reject) => {
var chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
export default async function handler(req, res) {
var rawBody = await getRawBody(req);
var signature = req.headers['x-paystack-signature'];
var expected = crypto.createHmac('sha512', process.env.PAYSTACK_WEBHOOK_SECRET).update(rawBody).digest('hex');
if (expected !== signature) return res.status(401).end();
var event = JSON.parse(rawBody);
if (event.event === 'charge.success') {
var txn = event.data;
var { productId } = txn.metadata;
await db.orders.upsert({ reference: txn.reference }, { status: 'paid', productId, email: txn.customer.email });
}
res.sendStatus(200);
}
See the Paystack integrations guide for platform-specific plugins.
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