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.
Frequently asked questions
Why does Payfast expect +
for spaces instead of %20
in the signature string?
+%20Payfast uses an older URL encoding standard, specifically RFC 1738. This standard dictates that spaces should be represented by a plus sign (
+%20encodeURIComponentCan I skip the passphrase if I'm only testing in the Sandbox environment?
While the Sandbox environment might sometimes let you get away with not setting a passphrase, it's a bad habit. The Live environment absolutely requires it. Always configure a passphrase for both Sandbox and Live in your Payfast dashboard and
.env.localWhy is getting the raw body so important for ITN verification in Next.js Route Handlers?
Next.js Route Handlers, like other frameworks, can re-parse or re-encode incoming request bodies for convenience. This processing can change the order of parameters, add/remove whitespace, or alter encoding. Payfast's signature verification is extremely sensitive to these exact details, requiring the raw, untouched POST data to accurately regenerate the hash.
What are common pitfalls with amount formatting that could cause a signature mismatch?
Floating point numbers are tricky. Payfast expects amounts as strings with exactly two decimal places, for example, "100.00". If you pass a raw number like
100100100.0099.999