Fixing Payfast 'Signature Mismatch' in Next.js
April 4, 2026Integrating Payfast into a Next.js application should be straightforward, but many developers hit a wall with the dreaded "Signature Mismatch" error. This usually happens during the ITN (Instant Transaction Notification) phase or when redirecting the user to the payment engine. In a modern Next.js environment—especially with the App Router—the way you handle request bodies and environment variables can easily break Payfast's hashing requirements.
As a senior consultant helping South African businesses scale their e-commerce, I've seen this issue crop up repeatedly. Here is the definitive guide to fixing it.
The Root Cause: Consistency is Key
The signature mismatch occurs when the hash you generate locally doesn't match the hash Payfast generates. Payfast expects a specific set of parameters, in a specific order, concatenated and MD5 hashed (optionally with a passphrase).
In Next.js, the three most common culprits are:
- Passphrase Mismatch: Forgetting to include the passphrase or using the wrong one for the environment (Sandbox vs. Live).
- Body Parsing Issues: Next.js API routes (or Route Handlers) might parse the incoming ITN body in a way that changes the order of keys or adds/removes whitespace.
- URL Encoding: Payfast expects raw POST data. If your code or a middleware re-encodes the data before hashing, the signature will fail.
Step 1: Securely Handling Your Passphrase
First, ensure your
.env.localPAYFAST_MERCHANT_ID=10001234 PAYFAST_MERCHANT_KEY=abc123def456 PAYFAST_PASSPHRASE=your_secret_passphrase PAYFAST_URL=https://sandbox.payfast.co.za/eng/process
Step 2: Generating the Initial Payment Signature
When you send a user to Payfast, you need to generate a signature. Here’s a robust utility function for Next.js:
import crypto from 'crypto'; export const generatePayfastSignature = (data: any, passphrase?: string) => { // 1. Create parameter string let getString = ""; for (const key in data) { if (data.hasOwnProperty(key) && data[key] !== "" && key !== 'signature') { getString += `${key}=${encodeURIComponent(data[key].toString().trim()).replace(/%20/g, "+")}&`; } } // 2. Remove last ampersand getString = getString.substring(0, getString.length - 1); // 3. Add passphrase if (passphrase) { getString += `&passphrase=${encodeURIComponent(passphrase.trim()).replace(/%20/g, "+")}`; } // 4. Hash return crypto.createHash('md5').update(getString).digest('hex'); };
Note: The
.replace(/%20/g, "+")%20Step 3: Handling the ITN (The Tricky Part)
The ITN is a POST request from Payfast to your server. In Next.js Route Handlers (
app/api/payfast/itn/route.tsimport { NextResponse } from 'next/server'; import crypto from 'crypto'; export async function POST(req: Request) { const formData = await req.formData(); const data: Record<string, string> = {}; formData.forEach((value, key) => { if (key !== 'signature') { data[key] = value.toString(); } }); const receivedSignature = formData.get('signature'); const generatedSignature = generatePayfastSignature(data, process.env.PAYFAST_PASSPHRASE); if (receivedSignature !== generatedSignature) { console.error('Signature Mismatch!'); return new NextResponse('Invalid Signature', { status: 400 }); } // Signature valid - Process order const paymentStatus = data['payment_status']; if (paymentStatus === 'COMPLETE') { // Update your DB: mark order as paid } return new NextResponse('OK', { status: 200 }); }
Debugging the Mismatch
If you are still getting a mismatch:
- Check the Order: The parameters must be in the exact order they were sent. Payfast usually sends them back in the same order, but it's safer to not sort them unless you are explicitly following Payfast's documentation on sorting.
- Floating Points: Ensure amounts are strings with two decimal places (e.g., "100.00"). Avoid passing raw numbers which might be formatted differently by JS.
- Sandbox vs Live: The Sandbox environment often ignores the passphrase if it's not set, but the Live environment requires it. Always use a passphrase for both.
Summary
The "Signature Mismatch" is almost always a result of how the string-to-be-hashed is constructed. By using
encodeURIComponent%20+Need help with more complex payment flows or custom checkout logic in South Africa? Reach out—I specialize in fixing these exact architectural headaches.