Bonaventure OgetoBy Bonaventure Ogeto|

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

#CheckWhat to Look ForCommon AI Mistake
1HMAC headerreq.headers['x-paystack-signature']Using x-paystack-hash or Authorization header
2HMAC bodyRaw Buffer, before JSON parsingUsing JSON.stringify(req.body) — fails HMAC
3HMAC algorithmsha512 (not sha256)Using sha256 — Paystack requires sha512
4Amount sourcePrice from your DB for the referenceTrusting req.body.data.amount from webhook
5IdempotencyCheck DB for reference before processingNo duplicate check — processes the same charge twice
6Secret keyprocess.env.PAYSTACK_SECRET_KEYHardcoded string — catastrophic if committed to git
7Response timingres.sendStatus(200) before await db.update()Await database before responding — causes Paystack retries on slow DB
8Kobo conversionNGN amount × 100 when calling PaystackUsing NGN amount directly — charges 100× less than intended
9Status checkVerifying data.status === 'success' after verify callProcessing on verify call 200 response without checking status
10Webhook URLHTTPS endpoint in production configHTTP 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.

Sign up for the McTaba newsletter

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