AI payment code review checklist: 1) Webhook HMAC — uses x-paystack-signature header, computes HMAC-SHA512 of raw body (not parsed JSON body). 2) Amount — price fetched from your database server-side, not from request body. 3) Idempotency — duplicate webhook events (same reference) detected and skipped. 4) Error handling — Paystack API errors surfaced and logged, not silently ignored. 5) Secret key — loaded from environment variable, not hardcoded. 6) HTTPS — webhook URL is HTTPS in production. 7) Response timing — webhook handler returns 200 before async processing. 8) No client-side verification — verification only server-side.
| # | Check | What to Look For | Common AI Mistake |
|---|---|---|---|
| 1 | HMAC header | req.headers['x-paystack-signature'] | Using x-paystack-hash or Authorization header |
| 2 | HMAC body | Raw Buffer, before JSON parsing | Using JSON.stringify(req.body) — fails HMAC |
| 3 | HMAC algorithm | sha512 (not sha256) | Using sha256 — Paystack requires sha512 |
| 4 | Amount source | Price from your DB for the reference | Trusting req.body.data.amount from webhook |
| 5 | Idempotency | Check DB for reference before processing | No duplicate check — processes the same charge twice |
| 6 | Secret key | process.env.PAYSTACK_SECRET_KEY | Hardcoded string — catastrophic if committed to git |
| 7 | Response timing | res.sendStatus(200) before await db.update() | Await database before responding — causes Paystack retries on slow DB |
| 8 | Kobo conversion | NGN amount × 100 when calling Paystack | Using NGN amount directly — charges 100× less than intended |
| 9 | Status check | Verifying data.status === 'success' after verify call | Processing on verify call 200 response without checking status |
| 10 | Webhook URL | HTTPS endpoint in production config | HTTP URL in webhook setup — rejected by Paystack in live mode |
// Reference: correct webhook handler pattern to compare against generated code
app.post('/webhook/paystack', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Return 200 immediately
res.sendStatus(200);
// 2. HMAC validation using raw body (Buffer) and sha512
var sig = req.headers['x-paystack-signature'];
var expected = crypto.createHmac('sha512', process.env.PAYSTACK_WEBHOOK_SECRET)
.update(req.body) // req.body is Buffer because of express.raw
.digest('hex');
if (sig !== expected) return; // silently ignore — already returned 200
var event = JSON.parse(req.body);
if (event.event !== 'charge.success') return;
// 3. Process asynchronously after response
setImmediate(async () => {
var ref = event.data.reference;
// 4. Idempotency check
var existing = await db.orders.findByReference(ref);
if (!existing || existing.status === 'paid') return;
// 5. Verify with Paystack API (do not trust webhook amount)
var verify = await fetch('https://api.paystack.co/transaction/verify/' + ref, {
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY },
});
var data = (await verify.json()).data;
if (data.status !== 'success') return;
// 6. Fulfill using amount from your DB, not from webhook
var order = await db.orders.findByReference(ref);
await db.orders.markPaid(ref);
await fulfillOrder(order);
});
});
See generating Paystack integration code with Claude Code for how to prompt for better output before reviewing.
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