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 = "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 .= "Host: www.paypal.com\r\n";//or www.sandbox.paypal.com
$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 = "
Buyer:
";
$mail_message .= $first_name." ".$last_name."
";
$mail_message .= $address_street."
";
$mail_message .= $address_posttown."
";
$mail_message .= $address_county."
";
$mail_message .= $address_postcode."


";

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

$mail_message .= "
";

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

//Build an email to the buyer

$mail_Subject2 = "Thank you for your purchase";

$mail_message2 = "
Dear ".$first_name."
";
$mail_message2 .= "
Thank you for your order - the details are confirmed below. I'll let you know when I've popped it in the post.

";

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

$mail_message2 .= "

Total: &pound;".$payment_total."
";
$mail_message2 .= "
Regards
Your Name 
";

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 = "
Workshop booked by:
";
$mail_message .= "
".$first_name." ".$last_name."
";
$mail_message .= $address_street."
";
$mail_message .= $address_posttown."
";
$mail_message .= $address_county."
";
$mail_message .= $address_postcode."

";

$mail_message .= "
Workshop: ".$workshop_name." Payment: &pound;".$payment_total."
";

$mail_message .= "
Number of places: ".$quantity."
";
$mail_message .= "";

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

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

$mail_message2 = "
Dear ".$first_name."
";
$mail_message2 .= "
Thank you for your booking - the details are confirmed below. 
";
$mail_message2 .= "
Workshop: ".$workshop_name."
";
$mail_message2 .= "
Payment: &pound;".$payment_total."
";
$mail_message2 .= "
Number of places: ".$quantity."
";

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

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

41 responses to “Paypal IPN listener in PHP”

  1. Simon says:

    Hi Graeme. If it were me I’d be looking into why your cURL responses aren’t getting through, especially since this was working previously, rather than changing approach. It is quite a while since I wrote this…..

  2. Hi Simon, When my site was hosted on a different server I had a response code that used cURL to post the response. Since moving to a new server, my responses aren’t getting through and paypal keeps resending the IPN (although my original code is getting the message and updating my database). Would you be confident that your approach will mean my responses are sent to paypal, or is it possible that whatever issue is preventing my responses in cURL will also affect your responses in php?

  3. Simon says:

    As far as I am aware you can only set one single URL on your site to handle IPN messaging – so this is the issue that you are facing, as both your applications really need their own discrete URL. What you really need is a single IPN address which will handle both types of response from Paypal. If you are in WordPress then this is harder than normal depending on the details of your setup, as you may need to either edit IPN Handler A to deal with a B type response, or create a brand new handler that handles both types. In either circumstance you’ll end up compromising your plugin upgrades somewhat. I’m not sure why your Paypal IPN setting is being turned off. I think the tack would be to concentrate on just one of the two first and aim to get that working before adding any additional complexity. Hope that helps.

  4. Mike says:

    Hi Simon, thanks for the coding info above, it seems to address a problem I may be having. Unfortunately I’m not a coder and this is a bit advanced for me to implement (I have no idea where to put your code), so I have questions I think are related.

    I’m trying to use two different methods, only one is a plugin for wordpress to hopefully achieve:

    A deposit payment from a reservations platform, SuperSaas; and an editable payment for the balance after an invoice is submitted, using a WPForm.
    When IPN is turned on at Paypal it instantly turns off again.

    The WPForm has given me a URL to enter for the IPN, there is no URL element for SuperSaas, so I assume it is self contained. Can both work if I have to set a URL on Paypal? Would I still receive an email from SuperSaas if a URL for the form is set on Paypal?

    I’m at the stage of thinking it would be easier to not have the balance payment option available via paypal.

    Thank you for your time.

  5. Simon says:

    Hi Roy
    It’s not clear what if anything you’re getting in debug2. If this is written OK, then you’ve connected OK. If not then check the connection string. Are you connecting via SSL?

  6. Roy says:

    Hi,when i use this in sandbox hitting the send ip all works at top of page says IPN was sent and the handshake was verified.
    debug1 file shows what was sent. But do not VERIFIED or anything in $response so nothing exicuted in VERIFIED

  7. Simon says:

    If everything else is working fine, then it sounds like there’s just a problem with the path to the files, or just possibly permissions on those files/folders. I don’t have any other suggestions, sorry.

  8. Josh says:

    ok, i just made the files, but still nothing is written to them. I am using php4 on redhat 9. i am trying to setup a system for a server i am running and can’t migrate to another system.

  9. Simon says:

    That sounds like there’s just an issue with writing the log files. Is the path to the file(s) correct?

  10. Josh says:

    I am trying to use this script, but when testing with ipn simulator on paypal, i dont even get debug txt files. I dont get any errors, but the files don’t get written either.
    in addition, the simulator says ipn sent, and handshake verified.

  11. Simon says:

    Goran
    In the first part of your code the response from Paypal is $resp, but in the second part you are checking $readresp. Is that the issue?

  12. 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?

  13. 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:

    http://go4vox.com/listener.php

    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.

  14. Simon says:

    Michael
    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.

  15. 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.

  16. 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

  17. Simon, thank you for the information, it is now working like a champ.
    I appreciate you providing this resource.

  18. 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://www.paypal.com', 443, $errno, $errstr, 30);
  19. I am trying to use the script, but it shows nothing at debug/debug3_fgets.txt
    So it seems the posting back to sandbox.Paypal.com is not bringing anything back.

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

  20. 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.

  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. 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: http://www.sandbox.paypal.com\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!

  23. 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.

  24. 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?

  25. 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 ('www.paypal.com', 80, $errno, $errstr, 30);

    with this

    $fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30);
  26. 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.

  27. Simon says:

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

  28. David Reynolds says:

    Simon,
    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,
    David.

  29. 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.

  30. 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.. .

  31. 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.

  32. 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.

  33. 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.

  34. 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

  35. 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.

  36. 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

  37. Simon says:

    Thanks Hans, I’ve corrected that.

  38. 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

  39. Simon says:

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

  40. Reece says:

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

  41. 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 …

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.