Reviewing AI-Generated Payment Code: A Checklist
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.
Full Review Checklist
| # | 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);
});
});
Learn More
See generating Paystack integration code with Claude Code for how to prompt for better output before reviewing.
Key Takeaways
- ✓HMAC validation must use the raw request body as a Buffer, not JSON.stringify(req.body).
- ✓Amount must come from your database, not from the client or the webhook payload amount field.
- ✓Duplicate webhook events are common — check for the reference in your DB before processing.
- ✓Secret key must load from process.env or equivalent — never hardcoded in source code.
- ✓Return HTTP 200 from the webhook endpoint before any async processing to prevent Paystack retries.
Frequently Asked Questions
- How do I test the HMAC validation in generated code?
- Use Paystack's webhook test tool in the dashboard (Settings → API Keys → Test Webhooks) to send a real event to your local server via ngrok. A valid signature should pass. Then manually modify the request body before sending and verify the handler rejects it. If the handler passes a modified body, the HMAC validation is broken — the most common cause is using JSON.stringify(req.body) instead of the raw Buffer.
- What if the generated code does not include raw body parsing?
- Add it manually. For Express: register the webhook route with <code>express.raw({ type: "application/json" })</code> middleware, and make sure this middleware applies before your global <code>express.json()</code> middleware. For Next.js App Router: use the Request object's .text() method before JSON.parse() to get the raw body. Generated code often misses this because it is not obvious that the middleware order matters.
- Is it safe to deploy AI-generated Paystack code to production?
- With review, yes. Without review, no. AI-generated payment code is often 90% correct — which means one critical security issue every 10 implementations. The checklist in this article covers the issues that appear most often. Run through it for every generated webhook handler and transaction initialization before deploying. The review takes 10 minutes and prevents issues that take days to diagnose in production.
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