5. ITN Callback

Instant Transaction Notification

ITN callback flow

Note: This page is never viewed by the buyer and any HTML output from it is inconsequential.

The website page that will be called to begin the exchange of data between the two web servers is set by the notify_url field on the confirm page. On this page, the following process should be followed:

  1. Receive the data posted by PayFast
  2. Notify PayFast that the information has been received
  3. Perform security checks
    • Verify the security signature is valid
    • Verify the source IP address belongs to PayFast
    • Verify the payment amount matches your order amount
    • Verify the data received is valid
  4. Verify that the order hasn’t been processed already
  5. Process the order
    • Update the status to paid
    • Email the buyer confirming payment

Step one

Receive the payment information from PayFast and then tell PayFast that this page is reachable by triggering a header 200, the payment engine will make a few attempts, one immediately and then one after 10 minutes again, then exponentially at longer intervals, until it receives an OK 200 from your web server. Once the ITN receives the OK 200 there will be no further comminication with the ITN.

header( 'HTTP/1.0 200 OK' );

Store the posted data in:

define( 'SANDBOX_MODE', true );
$pfHost = SANDBOX_MODE ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
// Posted variables from ITN
$pfData = $_POST;
// Strip any slashes in data
foreach( $pfData as $key => $val )
    $pfData[$key] = stripslashes( $val );

Step two

Conduct some security checks to ensure that the data you are receiving is correct, from the correct source and hasn’t been altered; you should not continue the process if a test fails!

Security check one

Verify the security signature in the data array; this is done in a similar way that the signature that you generated for stage one of the user payment flow. It is to ensure that there hasn’t been any middle man attacks and changing of values within the received data. The string that gets created needs to include all fields posted from PayFast.If a passphrase has been set in the PayFast Settings, then it needs to be included in the signature string.

// $pfData includes all the fields posted through from PayFast, this includes the empty strings
foreach( $pfData as $key => $val )
    if( $key != 'signature' )
        $pfParamString .= $key .'='. urlencode( $val ) .'&';
// Remove the last '&' from the parameter string
$pfParamString = substr( $pfParamString, 0, -1 );
$pfTempParamString = $pfParamString;
// If a passphrase has been set in the PayFast Settings, include it in the signature string.
$passPhrase = ''; //You need to get this from a constant or stored in you website database
if( !empty( $passPhrase ) )
    $pfTempParamString .= '&passphrase='.urlencode( $passPhrase );
$signature = md5( $pfTempParamString );
    die('Invalid Signature');

Security check 2

With this test you will be checking to ensure that your application is communicating with a valid PayFast payment engine.

The following is a list of valid domains:

  • www.payfast.co.za
  • w1w.payfast.co.za
  • w2w.payfast.co.za
  • sandbox.payfast.co.za


// Variable initialization
$validHosts = array(
$validIps = array();
foreach( $validHosts as $pfHostname )
    $ips = gethostbynamel( $pfHostname );
    if( $ips !== false )
        $validIps = array_merge( $validIps, $ips );
// Remove duplicates
$validIps = array_unique( $validIps );
if( !in_array( $_SERVER['REMOTE_ADDR'], $validIps ) )
    die('Source IP not Valid');

Security check three

Check payment data against the merchant’s order. This can be determined by comparing the amount processed for payment and the amount of the cart in you application and seeing if they match.

$cartTotal = xxxx; //This amount needs to be sourced from your application
if( abs( floatval( $cartTotal ) - floatval( $pfData['amount_gross'] ) ) > 0.01 )
    die('Amounts Mismatch');

Security check four

Using cURL or fsockopen, validate the data that you have received from PayFast by contacting our server and confirm order details.


if( in_array( 'curl', get_loaded_extensions() ) )
    // Variable initialization
    $url = 'https://'. $pfHost .'/eng/query/validate';
    // Create default cURL object
    $ch = curl_init();
    // Set cURL options - Use curl_setopt for freater PHP compatibility
    // Base settings
    curl_setopt( $ch, CURLOPT_USERAGENT, PF_USER_AGENT ); // Set user agent
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); // Return output as string
    curl_setopt( $ch, CURLOPT_HEADER, false ); // Don't include header in output
    curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
    curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
    // Standard settings
    curl_setopt( $ch, CURLOPT_URL, $url );
    curl_setopt( $ch, CURLOPT_POST, true );
    curl_setopt( $ch, CURLOPT_POSTFIELDS, $pfParamString );
    curl_setopt( $ch, CURLOPT_TIMEOUT, PF_TIMEOUT );
    if( !empty( $pfProxy ) )
        curl_setopt( $ch, CURLOPT_PROXY, $proxy );
    // Execute CURL
    $response = curl_exec( $ch );
    curl_close( $ch );
    $header = '';
    $res = '';
    $headerDone = false;
    // Construct Header
    $header = "POST /eng/query/validate HTTP/1.0\r\n";
    $header .= "Host: ". $pfHost ."\r\n";
    $header .= "User-Agent: ". PF_USER_AGENT ."\r\n";
    $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $header .= "Content-Length: " . strlen( $pfParamString ) . "\r\n\r\n";
    // Connect to server
    $socket = fsockopen( 'ssl://'. $pfHost, 443, $errno, $errstr, PF_TIMEOUT );
    // Send command to server
    fputs( $socket, $header . $pfParamString );
    // Read the response from the server
    while( !feof( $socket ) )
        $line = fgets( $socket, 1024 );
        // Check if we are finished reading the header yet
        if( strcmp( $line, "\r\n" ) == 0 )
            // read the header
            $headerDone = true;
        // If header has been processed
        else if( $headerDone )
            // Read the main response
            $response .= $line;
$lines = explode( "\r\n", $response );
$verifyResult = trim( $lines[0] );
if( strcasecmp( $verifyResult, 'VALID' ) != 0 )
    die('Data not valid');

Step three

Verify that the order hasn’t been processed already.

$pfPaymentId = $pfData['pf_payment_id'];
//query your database and compare in order to ensure you have not processed this payment already


Step four

Once you have completed these tests and the data received is valid, check the payment status and handle appropriately.

switch( $pfData['payment_status'] )
    case 'COMPLETE':
    // If complete, update and process as necessary                    
    case 'FAILED':                    
    // There was an error, update your application as necessary
    case 'PENDING':
    // The transaction is pending, please contact PayFast's support team for further assistance
    // If unknown status, do nothing (safest course of action)


I’m getting a signature mismatch error, why?

This is most likely caused if you generated the md5 hashed string with the variables in the wrong order, they need to be in the order as they appear in the tables above. Another reason could be that you have not urlencoded the variable values and trimmed all white spaces off the ends using php’s trim() function. The resultant urlencoding must be in upper case (eg. http%3A%2F%2F), and spaces encoded as ‘+’.

Why am I not receiving the ITN callback?

There could be many reasons for this to occur:

  • You’ve modified Pay Now button code

ITN callbacks are only triggered for standard integration and are not triggered for Pay Now button integration. If you have taken Pay Now button code and modified it to include the notify_url value for the purposes of receiving an ITN callback, it will not work.

Specifically if you have included the two fields below in your HTML form posted to PayFast, ITN will not be triggered (even if you’ve specified the notify_url field):

<input name="cmd" type="hidden" value="_paynow" />
<input name="receiver" type="hidden" value="[EMAIL_ADDRESS]" />

You need to change these two fields to rather use your merchant identifiers as follows:

<input name="merchant_id" type="hidden" value="[MERCHANT_ID]" />
<input name="merchant_key" type="hidden" value="[MERCHANT_KEY]" />
Remember to remove the cmd and receiver fields!
  • Your ITN page is unreachable

The URL specified by your notify_url variable could be unreachable.

To test this, open a browser and navigate to the URL yourself. Ensure that the page is in fact reachable and returns a valid HTML response (200 OK etc.).

If your browser times out or gets a 500 error response, then the page is unreachable and indicates that there could be a problem with your web server configuration. Contact your system administrator to resolve this.

  • Your ITN page performs a 302 redirect

If your notify page tries to redirect to another page immediately, it might not look like your are receiving the ITN callback, as, for security reasons, ITN does not follow redirects.

Ensure that all the processing which needs to occur, occurs at the notify_url location itself without requiring a redirect to another location.

Typical instances where this occurs without your knowledge, is where your notify_url is actually protected by a login mechanism and when the PayFast server tries to POST to it, the system redirects to a login page asking for a username and password.

Test this, by clearing all session cookies (or using a different browser) and navigating to the page yourself. If it redirects you to another page, that’s your problem.

  • You are making use of an incorrect port

PayFast makes use of ports 80, 8080, 8081 and 443 only.

I get the error ‘invalid Merchant ID

There could be a number of reasons for this:

  • You are using the sandbox (test) credentials on the live site

By default, our payment modules use the sandbox credentials and are set to point to the sandbox site.

Check your payment module configuration and ensure that you are using your live credentials and that you are set to use the live site.

Your Merchant ID and Merchant Key are available on the integration page.

  • Your account type isn’t eligible to receive payments

Only Individual, Business or Cause accounts are issued Merchant IDs and Merchant Keys. Check your account type on your Account page. If you don’t have the required account type, you’d need to click the upgrade account button and follow the system prompts to provide any missing information.

Note: Your account may have been downgraded during Oct/Nov 2009 which is why it is no longer a Business account

All you need to do is provide the outstanding documentation to upgrade your account again. Please note that your Merchant Key will change during the upgrade process and you’ll need to re-enter it into your payment module configuration.