Need some help?

I'm usually available for small jobs or problem solving with jQuery or css, at reasonable rates. Just get in touch.

Web Hosting

We recommend Clook for web hosting. UK based, great service and great value.

Paypal IPN listener in PHP

I spent quite a while messing about with code trying to get a Paypal IPN listener working correctly, and a lot of the stuff I found on the web didn’t work correctly, so here’s what I ended up with in case anyone else finds it useful. It’s freely adapted and significantly extended from this page at DesignerTuts.

The code below is commented to be reasonably self explanatory, but a few points worthy of note.

I’m expecting two possible types of message from Paypal, one (which which has $purchase_type of “shop”) which may contain one or more items bought from an online shop, and another ($purchase_type of “workshop”) which contains a booking for one or more spaces on a single workshop.

I’m only processing IPN messages which are Verified and Completed – this code does not deal with refunds or errors.

I found the best way to debug problems while setting this up was to get the file to dump text files at various points in the process – so I could see how far things had got. In some cases the files include some useful debug info. These can be toggled on and off via the $debug variable at the top of the code.

The code is triggered by a post from Paypal IPN, and code does the following:

  1. Connects to my database, sets up user defined variables
  2. Extracts the data from the post
  3. Opens a connection to Paypal and posts all the variables back again
  4. If the response from Paypal is VERIFIED, reads all the post data into local variables, and…
  5. …if it’s a shop purchase:
    • sends an email to the site owner detailing the transaction
    • sends an email to the purchaser confirming the purchase
    • updates the database to mark the item as sold
  6. …or if it’s a workshop booking:
    • sends an email to the site owner detailing the booking
    • sends an email to the purchaser confirming the booking
    • updates the database to reduce the available spaces on the workshop, and record the email of the booker

Here’s the code. Feel free to offer comments or suggestions for improvement.

<?php 
require("db_connect.php"); // this holds our database connection credentials
// Paypal Posts HTML Form variables to this page - we will post them back with an extra parameter cmd with value _notify-validate
//DEBUGGING - set this to true to write debug files
$debug = true;

// set up variables for our own local settings
$account_owner = "your_data@here.com"; //Initialise Paypal account holder
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=utf-8' . "\r\n";
$headers .= "From: your_data@here.com"; //Initialise email from which emails will be sent
$mail_To = "your_data@here.com"; //Enter the email for alerts and confirmations

//Build the data to post back to Paypal
$postback = 'cmd=_notify-validate'; 
// go through each of the posted vars and add them to the postback variable
foreach ($_POST as $key => $value) {
$value = urlencode(stripslashes($value));
$postback .= "&$key=$value";
}

//DEBUGGING - export a text file with all the post data on it
if ($debug)
{
$ourFileName = "debug/debug1_postdata.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fwrite($ourFileHandle, $postback);
fclose($ourFileHandle);
}

// build the header string to post back to PayPal system to validate
$header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($postback) . "\r\n\r\n";

// Send to paypal or the sandbox depending on whether you're live or developing
// comment out one of the following lines
//$fp = fsockopen ('www.sandbox.paypal.com', 80, $errno, $errstr, 30);//open the connection
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
// or use port 443 for an SSL connection
//$fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30);

if (!$fp) 
{
// HTTP ERROR Failed to connect
//error handling or email here

 }

else // if we've connected OK
{
 //DEBUGGING - export a text file to show we've connected OK
if ($debug)
{
$ourFileName = "debug/debug2_connected.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fclose($ourFileHandle);
}

fputs ($fp, $header . $postback);//post the data back
while (!feof($fp)) 
{
$response = fgets ($fp, 1024);

 //DEBUGGING - export a text file containing the response
if ($debug)
{
$ourFileName = "debug/debug3_fgets.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fwrite($ourFileHandle, $response);
fclose($ourFileHandle);
}

if (strcmp ($response, "VERIFIED") == 0) 
{//It's verified

//DEBUGGING - export a text file to confirm verification
if ($debug)
{
$ourFileName = "debug/debug4_verified.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fclose($ourFileHandle);
}

// assign posted variables to local variables, apply urldecode to them all at this point as well, makes things simpler later
 $txn_type = $_POST['txn_type'];//read the type of payment
$purchase_type = $_POST['custom'];//this is a custom variable as we're using this for two different sorts of payments

$i=1;
while (isset($_POST['item_number'.$i]))//read the item details
{
$item_ID[$i]=$_POST['item_number'.$i];
$item_name[$i]=urldecode($_POST['item_name'.$i]);
$item_cost[$i]=$_POST['mc_gross_'.$i];
$i++;
}
$item_count = $i-1;
$workshop_name = urldecode($_POST['item_name']);//only one item means no cart so workshop
$workshopid = urldecode($_POST['item_number']);//ditto, for id
$quantity = $_POST['quantity'];

$payment_status = $_POST['payment_status'];//read the payment details and the account holder
$payment_currency = $_POST['mc_currency'];
$payment_total = $_POST['mc_gross']; 
$posted_account_owner = urldecode($_POST['receiver_email']);
$buyer_email = urldecode($_POST['payer_email']);//read the buyer details
$first_name = urldecode($_POST['first_name']);
$last_name = urldecode($_POST['last_name']);
$address_street = urldecode($_POST['address_street']);
$address_posttown = urldecode($_POST['address_city']);
$address_county = urldecode($_POST['address_state']);
$address_postcode = urldecode($_POST['address_zip']);

//DEBUGGING - export a text file to check the confirmation
if ($debug)
{
$ourFileName = "debug/debug5_confirmedok.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fclose($ourFileHandle);
}
 // further checks
 if(($payment_status == 'Completed') && //payment_status = Completed
($posted_account_owner == $account_owner) && //comes from the right account
($purchase_type == "shop" || $purchase_type == "workshop") && //is of the type that we expect 
($payment_currency == "GBP")) // and in the right currency 
 {

// if we've reached this point all is well, now we can send emails and update databases with confidence
//DEBUGGING - export a text file to check the payment status
if ($debug)
{
$ourFileName = "debug/debug6_statuscompleted.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fclose($ourFileHandle);
} 

//Build an email to the shop owner

if ($purchase_type == "shop")//here's the stuff for a shop purchase
{
$mail_Subject = "A purchase has been completed from your shop";

$mail_message = "<html><head></head><body style=\"font-family:Arial,Helvetica,sans-serif;font-size:12pt\"><p>Buyer:<br/>";
$mail_message .= $first_name." ".$last_name."<br/>";
$mail_message .= $address_street."<br/>";
$mail_message .= $address_posttown."<br/>";
$mail_message .= $address_county."<br/>";
$mail_message .= $address_postcode."<br/></p><p>";

for ($j=1;$j<=$item_count;$j++)
{
$mail_message .= "Item: ".$item_ID[$j]." ".$item_name[$j]." &pound;".$item_cost[$j]."<br/>";
}

$mail_message .= "</p></body></html>";

mail($mail_To, $mail_Subject, $mail_message, $headers);

//Build an email to the buyer

$mail_Subject2 = "Thank you for your purchase";

$mail_message2 = "<html><head></head><body style=\"font-family:Arial,Helvetica,sans-serif;font-size:12pt\"><p>Dear ".$first_name."</p>";
$mail_message2 .= "<p>Thank you for your order - the details are confirmed below. I'll let you know when I've popped it in the post.</p><p>";

for ($j=1;$j<=$item_count;$j++)
{
$mail_message2 .= $item_name[$j]." &pound;".$item_cost[$j]."<br/>";
}

$mail_message2 .= "</p><p>Total: &pound;".$payment_total."</p>";
$mail_message2 .= "<p>Regards<br/>Your Name </p></body></html>";

mail($buyer_email, $mail_Subject2, $mail_message2, $headers);

//Update the database
for ($j=1;$j<=$item_count;$j++)
{
$qstring="UPDATE items SET Sold = 'Sold' WHERE ID = '".$item_ID[$j]."'";
mysql_query($qstring);
}
}//end shop

if ($purchase_type == "workshop")//here's the stuff for a workshop booking
{
//Build an email to the workshop owner
$mail_Subject = "Workshop booking";

$mail_message = "<html><head></head><body style=\"font-family:Arial,Helvetica,sans-serif;font-size:12pt\"><p>Workshop booked by:</p>";
$mail_message .= "<p>".$first_name." ".$last_name."<br/>";
$mail_message .= $address_street."<br/>";
$mail_message .= $address_posttown."<br/>";
$mail_message .= $address_county."<br/>";
$mail_message .= $address_postcode."<br/></p>";

$mail_message .= "<p>Workshop: ".$workshop_name." Payment: &pound;".$payment_total."</p>";

$mail_message .= "<p>Number of places: ".$quantity."</p>";
$mail_message .= "</body></html>";

mail($mail_To, $mail_Subject, $mail_message, $headers);

//Build an email to the punter
$mail_Subject2 = "Thank you for your workshop booking";

$mail_message2 = "<html><head></head><body style=\"font-family:Arial,Helvetica,sans-serif;font-size:12pt\"><p>Dear ".$first_name."</p>";
$mail_message2 .= "<p>Thank you for your booking - the details are confirmed below. </p>";
$mail_message2 .= "<p>Workshop: ".$workshop_name."</p>";
$mail_message2 .= "<p>Payment: &pound;".$payment_total."</p>";
$mail_message2 .= "<p>Number of places: ".$quantity."</p>";

$mail_message2 .= "<p>I'll send out a reminder of venue etc. nearer the date. I look forward to seeing you at the workshop.</p>";
 $mail_message2 .= "<p>Regards<br/>Your name </p></body></html>";

mail($buyer_email, $mail_Subject2, $mail_message2, $headers);

 //Update the database
$qstring="UPDATE workshops SET Places = (Places-".$quantity."), Attendees = concat(Attendees,';".$buyer_email."') WHERE ID = '".$workshopid."'";
mysql_query($qstring);

//DEBUGGING - export a text file to check the update string
if ($debug)
{
$ourFileName = "debug/debug7_workshopupdate.txt";
$ourFileHandle = fopen($ourFileName, 'w') or die("can't open file");
fwrite($ourFileHandle, $qstring);
fclose($ourFileHandle);
}

}

mysql_close($con);
 
}
else //the Paypal response is VERIFIED but something else has failed - maybe it's a refund, or a different payment type
{
// optionally send an email
//error handling or email here  

 }
}
else if (strcmp ($response, "INVALID") == 0) 
{ 
//the Paypal response is INVALID, not VERIFIED
// This implies something is wrong 
// If this happens, enable debugging and start by look at the contents of debug1_postdata.txt

if ($txn_type != "")
{
//error handling or email here
}
}
} //end of while
fclose ($fp);
}
?>

11 responses to “Paypal IPN listener in PHP”

  1. I wrote an IPN listener but never tested it till now … and if don’t get the right result … may be i’ll use you listner …

  2. Reece says:

    Why use or die() messages.. its not like you can see the message.

  3. Simon says:

    Yes, fair comment. I used it while testing, but it’s not critical in live working.

  4. Hans says:

    ($posted_account_owner == $account_owner) && comes from the right account should be
    ($posted_account_owner == $account_owner) && // comes from the right account.
    But thx for your code. Using it for my tool

  5. Simon says:

    Thanks Hans, I’ve corrected that.

  6. Pacis Dream says:

    I have no t used this yet (was browsing.). A couple of observations:

    1. For debug/error, you should direct everything to a text file, and in case of error, send error to that file.

    2. You should probably have a variable called $debug, that’s set when calling the script ($debug = $_REQUEST['debug']).

    3. Using mail may not work due. You should use something else for that, like setting up a gmail account, and calling the gmail smtp interface

  7. Simon says:

    Um, I am using a $debug variable – this script is called from Paypal, and I’m not aware that you can set Paypal to set a debug flag.
    The script deliberately uses several text files, as if something fails the name of the text file helps identify where the problem is.
    I don’t see any issue with using mail. This has been used on a live implementation for over 2 years now with no problem whatsoever.

  8. whichrtmej says:

    Hi, I came across your script and would love to use it on my site. Right now I add the form info to my db then go to paypal for payment and it works fine, but I would like it the other way around. I am a novice at this and I am trying to implement it.

    I have debugging on. I commented out the mail and db sections and added my db info. When the script runs all works fine, until it get to:

    if (strcmp ($response, “VERIFIED”) == 0)
    {//It’s verified

    //DEBUGGING – export a text file to confirm verification
    if ($debug)
    {
    $ourFileName = “debug/debug4_verified.txt”;
    $ourFileHandle = fopen($ourFileName, ‘w’) or die(“can’t open file”);
    fclose($ourFileHandle);
    }

    I get debug file1 2 and 3 but nothing seems to get though after this. I have added a statement as above to write a debug file in this section

    else if (strcmp ($response, “INVALID”) == 0)

    but my debug file does not get written.

    Any help would be greatly appreciated.

    Thanks Whichrtmej

  9. Simon says:

    If it’s failing at that point I’d write $response to a debug file – since it seems likely that the content of that does not contain expected values.

  10. Charles says:

    Thanks Simon,
    A great script that is clear and it works.
    Just one question though. I am using the Paypal sandbox up to now but I am getting multiple entries in the database and multiple emails to the (Virtual) buyer for each test from the Instant Payment Notification (IPN) Simulator.
    Do you now why this happens?
    P.S. Happy New Year.

  11. Simon says:

    Hi Charles
    I guess the first thing to ascertain is whether you are getting multiple IPN messages that are each generating the email, or whether a single IPN message is generating multiple emails. You should be able to check this from Paypal via History > IPN History, which will show you the IPN messages. If there are multiple messages generated, then the problem is further back up the purchase process, obviously. You may already have confirmed this, not sure.

    If there is only a single message generating multiple emails then there’s some issue somewhere in the script itself – although I’ve been using this script live for a few years now, so there’s no fundamental issue, I think. You could investigate further by amending the logging slightly so that it (say) creates a file with a random name, or a file name constructed from the current time – then you can see how how many times the script is looping through the logic, which may help identify the issue.

Useful? Interesting? Leave me a comment

I've yet to find a way of allowing code snippets to be pasted into Wordpress comments - so if you're trying to do this you'd be better off using the contact form.