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.

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 = ""; //Initialise Paypal account holder
$headers = 'MIME-Version: 1.0' . "\r\n";
$headers .= 'Content-type: text/html; charset=utf-8' . "\r\n";
$headers .= "From:"; //Initialise email from which emails will be sent
$mail_To = ""; //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);

// build the header string to post back to PayPal system to validate
$header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
$header .= "Host:\r\n";//or
$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 ('', 80, $errno, $errstr, 30);//open the connection
$fp = fsockopen ('', 80, $errno, $errstr, 30);
// or use port 443 for an SSL connection
//$fp = fsockopen ('ssl://', 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");

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);

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");

// 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

while (isset($_POST['item_number'.$i]))//read the item details
$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");
 // 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");

//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]."'";
}//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."'";

//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);


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);

30 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”);

    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.

  12. James Irwin says:

    Thanks Simon for offering your help and understanding to those trying to work their way into producing a web site. Aside from the web, I have no one to refer to so it is a slow upward battle and progress is slow so I truly appreciate the effort you’ve given to provide this.. .

  13. Simon says:

    You’re welcome James. I’ve used this on a number of sites now – if you’re really stuck drop me a line here.

  14. David Reynolds says:

    I’d certainly echo James’s sentiment here. Apart from anything else, your script gives me a valuable insight into certain aspects of PHP that I’d not previously used.
    With your permission, I’d like to modify to use the PHPMailer script as this will allow me to send attachments to the purchaser.
    Thanks again,

  15. Simon says:

    Hi David.
    Thanks, you’re welcome to amend as needed.

  16. kevin says:

    hi simon,
    i’ve gotten the same problem @whichrtmej (comment 8) has been faced with and how to suggested the solution can’t really work since the data contained in the variable $response is not being output in the file.The debug file is empty.

  17. Simon says:

    Hi Kevin
    OK, so that tells us that $response is empty, in which case you’re not getting a response back from Paypal. First off, try changing your connection back to Paypal to use SSL. So replace this:

    $fp = fsockopen ('', 80, $errno, $errstr, 30);

    with this

    $fp = fsockopen ('ssl://', 443, $errno, $errstr, 30);
  18. James says:

    Hi there,

    I get the first debug file with the IPN data and then my second one is blank and then my third one has 2mb of blank space. Any idea of what is going wrong?

  19. Simon says:

    The second debug file should be blank – that just indicates you’ve connected OK back to the Paypal server. I’ve never come across a lot of blank space in the third file, but that suggests you’re not getting the expected response from Paypal. Try using an SSL connection if you’re not already.

  20. James says:

    Thank you Simon for getting back in touch, I solved the problem with the third file by adding the following to the headers.

    $header.= “Host:\r\n”;

    All works okay in the sandbox, hopefully should be fine in realtime. Great script, I have looked at many on the Internet and this is by far the best, the easiest and well explained. Credit to you Simon!

  21. Simon says:

    Thanks for that. I’ve updated the script to reflect the additional header data – think this has changed since I first wrote the article.

  22. Bill says:

    I have not implemented this yet, I would like to use it without the database. I only heave two items to sell, so I would like to keep is as simple as possible.
    1. Paypal returns to the listener page, verifies all
    2. emails send to owner, customer and fulfillment center
    Is that possible.
    Thank you for your help with this great script.

  23. I am trying to use the script, but it shows nothing at debug/debug3_fgets.txt
    So it seems the posting back to is not bringing anything back.

    I am using the IPN Simulator on Paypal developers test.
    Any help is appreciated.

  24. Simon says:

    Hi Bill
    Yes, you don’t have to update a database. Just remove that part of the code and add whatever you need. Re your Sandbox issue – are you using an SSL connection? Worth a try with that – uncomment this line:

    //$fp = fsockopen ('ssl://', 443, $errno, $errstr, 30);
  25. Simon, thank you for the information, it is now working like a champ.
    I appreciate you providing this resource.

  26. mo says:

    Simon may I just say thank you for this example . It has saved me so much time, I have adapted it to my own use. Many thanks Mo

  27. Michael says:

    I’m scouring the web, trying to find a way to make my IPN listener work. This script is the MOST helpful that I’ve found. THANKS!
    One question:
    Does the $response variable contain ALL the data coming back from PayPal? If so, it would, by definition, be larger than “VERIFIED”, and as a result, strcmp could not return a 0. Am I missing something here? Thanks.

  28. Simon says:

    The response is just ‘VERIFIED’. The original post from Paypal contains all the data, which is posted back to Paypal unchanged. The response will either be VERIFIED or INVALID.

  29. Valentin says:

    Please check your server that handles PayPal Instant Payment Notification (IPN) messages. Messages sent to the following URL(s) are not being received:

    If you do not recognize this URL, you may be using a service provider that is using IPN on your behalf. Please contact your service provider with the above information.
    Hi Simon,
    i adopted your script for my site but i’ve got two emails from paypal, with the text below, what sould i do? Please help…

    Once you or your service provider fix this problem, you or your service provider can resend the failed messages from the IPN History page. If this problem continues, PayPal may disable the IPN feature for your account.

  30. Simon says:

    Impossible to suggest what might be the issue here. Do you have Paypal enabled on your site? Are payments made on your store processes correctly? Are you using the correct Paypal code?

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.