Paystack with Strapi
In Strapi: create a custom route POST /api/payments/initialize that reads the product price from your Product content type, calls Paystack POST /transaction/initialize, and returns the authorization_url. Create POST /api/payments/webhook that validates HMAC and updates your Order content type to status "paid". Store PAYSTACK_SECRET_KEY in .env.
Strapi Custom Payment Routes
// src/api/payment/routes/payment.js (Strapi v4)
module.exports = {
routes: [
{ method: 'POST', path: '/payments/initialize', handler: 'payment.initialize', config: { auth: false } },
{ method: 'POST', path: '/payments/webhook', handler: 'payment.webhook', config: { auth: false } },
],
};
Strapi Payment Controller
// src/api/payment/controllers/payment.js (Strapi v4)
const crypto = require('crypto');
module.exports = {
async initialize(ctx) {
var { productId, email } = ctx.request.body;
// Fetch price from Strapi Product content type
var product = await strapi.entityService.findOne('api::product.product', productId, { fields: ['name', 'price'] });
if (!product) return ctx.notFound('Product not found');
var reference = 'order_' + productId + '_' + Date.now();
// Create order record in Strapi
await strapi.entityService.create('api::order.order', {
data: { reference, product: productId, customer_email: email, status: 'pending' },
});
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, amount: Math.round(product.price * 100), reference, currency: 'NGN' }),
});
var data = await res.json();
ctx.body = { url: data.data.authorization_url };
},
async webhook(ctx) {
var rawBody = ctx.request.rawBody; // requires Strapi raw body config
var signature = ctx.request.headers['x-paystack-signature'];
var expected = crypto.createHmac('sha512', process.env.PAYSTACK_WEBHOOK_SECRET).update(rawBody).digest('hex');
if (expected !== signature) return ctx.unauthorized();
var event = ctx.request.body;
if (event.event === 'charge.success') {
var orders = await strapi.entityService.findMany('api::order.order', {
filters: { reference: event.data.reference },
});
if (orders.length > 0) {
await strapi.entityService.update('api::order.order', orders[0].id, { data: { status: 'paid' } });
}
}
ctx.status = 200;
},
};
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Learn More
See the Paystack with headless CMS checkout guide for the full architecture.
Key Takeaways
- ✓Create custom Strapi routes for Paystack initialize and webhook — Strapi handles routing, you write the controller.
- ✓Fetch product price from Strapi's own Product content type — never trust frontend-supplied prices.
- ✓Store orders in a Strapi Order collection type and update status on charge.success webhook.
- ✓Keep PAYSTACK_SECRET_KEY and PAYSTACK_WEBHOOK_SECRET in Strapi's .env file.
- ✓Strapi v4 and v5 use different controller/router patterns — check your Strapi version.
Frequently Asked Questions
- How do I access the raw request body in Strapi for HMAC validation?
- Strapi v4 parses JSON bodies before your controller sees them — ctx.request.rawBody may be undefined. To fix this, configure Strapi's body parser to preserve the raw body: in config/middlewares.js, set the body parser's enabled option to keep rawBody. Alternatively, use a custom Koa middleware before the body parser to capture the raw buffer.
- Should I use Strapi v4 or v5 for a new Paystack project?
- Use Strapi v5 (latest) for new projects. The routing and controller patterns are similar to v4 but with some differences in entityService and query engine. The payment integration code above is v4 — for v5, use strapi.documents() API instead of strapi.entityService where needed.
- Can I use Strapi roles to protect the payment initialize route?
- Yes. If your storefront has authenticated users, set config.auth to require authentication on the initialize route. If the frontend user is anonymous (e.g., a public checkout), keep auth: false but validate input rigorously server-side. Never trust the client-supplied price or product details.
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