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;
},
};
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.
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