Fixing Payfast 'Signature Mismatch' in Next.js

April 4, 2026

Integrating 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:

  1. Passphrase Mismatch: Forgetting to include the passphrase or using the wrong one for the environment (Sandbox vs. Live).
  2. 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.
  3. 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.local
is correctly configured. You must set a passphrase in your Payfast dashboard and match it here.

PAYFAST_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, "+")
is critical. Payfast uses RFC 1738 encoding where spaces are pluses, not
%20
.

Step 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.ts
), you need to get the raw body to verify the signature accurately.

import { 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
with the
%20
to
+
replacement and ensuring your passphrase is correct, you'll eliminate 99% of these errors in your Next.js application.

Need help with more complex payment flows or custom checkout logic in South Africa? Reach out—I specialize in fixing these exact architectural headaches.


Related Articles