PHP Tutorial: PayPal Instant Payment Notification (IPN)

Got a letterIn a previous post I tried to give an introduction on how to get started with PayPal Payment Data Transfers (PDT). PDT is very handy in several cases, but you can’t always rely on it since it requires the user to return to your page after doing the payment. That will often happen, but it’s not guaranteed to happen. If you for example want to mark an order in your system as paid or something like that, you most likely want to use PayPal Instant Payment Notifications (IPN) in addition to PDT.

Instant Payment Notification (IPN) is a message service that notifies you of events related to PayPal transactions. You can use it to automate back-office and administrative functions, such as fulfilling orders, tracking customers, and providing status and other information related to a transaction. — PayPal

Once again the documentation, tutorials and code samples I found on this was a bit all over the place. Sort of messy and outdated. So, once again I decided to do my own thing and just follow the steps required and implement them myself. And since the tutorial on PDT turned out to be a bit of a success, I decided to share this too. Hopefully it can make the lives of fellow developers easier πŸ™‚

How it works

The concept is pretty simple. You first give PayPal an address to a listener, for example Then, whenever something happens, PayPal will post information about the event to that address. So for example if someone completes a payment, your listener will be notified shortly after with the transaction details and all you need to know.

This tutorial will focus on the listener part, as the setting up part on PayPal is very simple (Leave a comment if you disagree). Just go to the PayPal account settings, find the IPN settings and give the URL to your IPN listener. For testing we don’t even need to do that. All we need to do is to set up a PayPal Developer Account. We can then use their very handy Instant Payment Notification (IPN) simulator. So let’s get started!

Step 0: Sign up for a Developer Account

To access the IPN simulator, you need to login at Seems to be connected with the regular PayPal accounts now, so just try login with your actual PayPal credentials. If you don’t have a PayPal account, sign up for one πŸ™‚

Using the simulator is super simple, just enter the absolute URL to your listener and choose a transaction type. You can then fill out all the data that should be in this fake transaction and then just hit “Send IPN” at the bottom. Luckily PayPal actually fills out all the fields with random test data, so unless you are trying to test something specific, you can just ignore the data and hit the button right away πŸ™‚

Now, to implement the actual listener…

Step 1: Catch the IPN

First thing we need to do in our listener is of course to catch the data we get from PayPal. The data is sent as a POST request, so PHP has actually done that for us. We just need to grab it πŸ™‚

$ipn_post_data = $_POST;

Step 2: Verification

Since what we got could just be plain bogus from some stranger, we need to verify it with PayPal. This is done by taking all the POST data in its unaltered state, add one field to the beginning, and then send it back. In return we should then get one word, VERIFIED or INVALID. To do that, we can use our old buddy cURL.

// Choose url
if(array_key_exists('test_ipn', $ipn_post_data) && 1 === (int) $ipn_post_data['test_ipn'])
    $url = '';
    $url = '';

// Set up request to PayPal
$request = curl_init();
curl_setopt_array($request, array
    CURLOPT_URL => $url,
    CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $ipn_post_data),

// Execute request and get response and status code
$response = curl_exec($request);
$status   = curl_getinfo($request, CURLINFO_HTTP_CODE);

// Close connection

if($status == 200 && $response == 'VERIFIED')
    // All good! Proceed...
    // Not good. Ignore, or log for investigation...

Note that in the very beginning actually check for a field called test_ipn. If that exists with a value of 1, it means it’s a request from the sandbox. In other words, we can choose the correct PayPal interface by just looking for that.

Note: If the call to doesn’t work at all, curl might have problems with the SSL verification. This happened to me on one of my hosts, so documented it here.

Step 3: Fix character set

Now that we know the data is valid, we can start to deal with it. What you get could be in a different character set than what you want though, so we should fix that first. The data should contain a key called charset which specifies what character set the data is using. We just need to check for that and if needed convert from that to what we want, for example UTF-8.

if(array_key_exists('charset', $ipn_data) && ($charset = $ipn_data['charset']))
    // Ignore if same as our default
    if($charset == 'utf-8')

    // Otherwise convert all the values
    foreach($ipn_data as $key => &$value)
        $value = mb_convert_encoding($value, 'utf-8', $charset);

    // And store the charset values for future reference
    $ipn_data['charset'] = 'utf-8';
    $ipn_data['charset_original'] = $charset;

Step 4: Use the data!

Yup, believe it or not, that was everything that was needed to catch an IPN message from PayPal. Where you go from here completely depends on what exactly you need to do.

What you at least should do is the following:

  1. Confirm that the payment status is Completed.
    PayPal sends IPN messages for pending and denied payments as well, so don’t ship stuff or anything until the payment has cleared.
  2. Use the transaction ID to verify that the transaction has not already been processed.
    This prevents you from processing the same transaction twice. You can for example store the transaction id in a database and check against those before you do anything with incoming IPNs. If you’re smart you could also store the time the IPN came in and the raw IPN data. This way you have a log of all incoming messages you can use if you need to reprocess something or for debugging if something weird is going on.
  3. Make sure the receiver’s email address is the one you expected.
  4. Make sure the price, item description, et cetera, match what it should be.

And then finally you should of course make sure your customer gets what they paid for πŸ™‚

You can read more about PayPal Instant Payment Notifications in their IPN Guide.

Working sample

I have put together a working sample you can check out over at Hopefully this tutorial and that sample can help you get started with all of this πŸ™‚

The sample uses an IPN handler class I put together, that should be usable pretty much out of the box. Just extend the class, override the process method and do what you need. Below is the simple sample which just appends the incoming messages to a file.

<?php // example listener.php

require 'ipn_handler.class.php';

class My_Ipn_Handler extends IPN_Handler
    public function process(array $post_data)
        // Let the IPN_Handler do it's processing,
        // which includes validating and fixing the encoding
        $data = parent::process($post_data);

        // Check if validation failed
        if($data === FALSE)
            header('HTTP/1.0 400 Bad Request', true, 400);

        // Seems it all was good, so in lack of better things to do,
        // let's JSON encode it and dump it to a file
        file_put_contents('ipn.txt', json_encode($data).PHP_EOL, FILE_APPEND);

$handler = new My_Ipn_Handler();

That’s all! Hopefully someone find this useful. Learned a lot writing it at least! Please leave a comment if it was helpful, if it wasn’t helpful, if there are mistakes, typos, etc. πŸ™‚

  • great tutorial, thanks (:

  • I needed this info on May 29th, 2011 — good timing!

  • chandrasekhar

    Very detailed and good tutorial.

  • Gautam

    Thanks a lot!
    Newbie question – have replicated your sample from the samples site, but I’m not seeing any IPN messages show up and no log.txt being created. Is there something I may be missing?

    • Using the correct listener URL? Tried to just dump the $_POST array to a file to make sure you’re getting anything at all?

      • Josh

        I’m running into the same problem. I’m sure I’m missing an elementary step. This is what I did: I copied the source code for each of the 5 files and created those same files on my website’s server. I then use the IPN simulator to send an IPN to the listener.php at the url on my website server. But no .txt file is generated.

        The code is actually the easy part for me; getting things “setup” is the hard part…

        • Make sure the script has permission to create and write to the log file. Maybe that could be the problem.

          • Josh

            Thanks. I discovered that the problem is that my web server doesn’t support CURL. As someone who has never done something like this before, this discovery was hard to make (because I had NO IDEA how to even begin investigating–there’s no “f5” to press with a list of errors reported). If it’s not too much trouble, it might be worth adding a line in your tutorial that spells out some of the background setup instructions, such as that one must have a PHP working server that supports CURL. I’ve decided to switch to a different web hosting service.

            Thanks for caring for us newbies; your work is valued.

  • Yup, that’s what I was looking for.

  • Brian

    this is great, but I have one question. I’m using a SSL cert generated through godaddy, and they don’t use pem files for the certs, they use .crt files. I guess I just refer to the CRT file that godaddy generated?

    The odd thing about the way godaddy does this is that I didn’t even have to make a modification to my httpd.conf file.

    • There shouldn’t be any difference. The SSL certificate I talk about here is the one from PayPal, who we are talking to. What you are using for your website isn’t used for anything as far as I know.

      • Brian

        thanks for the quick reply. another question. I used your code above as a starting point, and I’m having problems. $status is returning 0, and $response is null. I tested the $url value, and it’s definitely sandbox. Only thing I can think of is that there’s a problem on my end with the way curl_setopt_array is initialized. I’m going to add this as a class to my classfile.php, but not until I get this working correctly. Does it matter which sandbox transaction type I’m using? I’m sending cart checkout (that’s what I’ll be using when I go live)

        • Try to check if you get any errors after you have executed the request. Could for example add this after you do it:

          • Brian

            this is totally kicking me in the hiney. I’ve read in a couple other BBS where if I’m not responding from an HTTPS server that it could be an issue, so I responded back to the PP using http…still nothing. At least now I’m getting “invalid” instead of zero. I’m fried. Been fighting this thing since about 0630 this morning, it’s now 1800 local.

          • You didn’t get any errors printed out? What about the status code? Is it 200 or something else?

            You should definitely talk to the HTTPS version of PP. What you are responding from shouldn’t matter at all though. My sample works fine, and I’m not using HTTPS. I have gotten it to work before too, also on a regular HTTP site.

          • Brian

            This is the error I got:
            error setting certificate verify locations:
            CAfile: cacert.pem
            CApath: none

          • Brian

            I removed the CAINFO value from the curl build and it worked. That’s really strange.

          • Were you missing the cacert.pem file perhaps? Either way, it may still work if you remove the CAINFO and the SSL_VERIFYPEER options. For me it worked on one server, but not on another, that’s why I added it to make it work on both πŸ™‚

  • Gregory

    Brilliant. I am making my way through it and it is very useful. However, I have noticed that when I include

    if($valid !== TRUE)
        return FALSE;

    in the ipn_handler.class.php file, I get a false reading. When I take it out, them the data is posted. Why would this not work in every situation? Also, is it detrimental to leave this out?

    • Well, I suppose that means $valid is not true. Try to do a var_dump($valid) or something. Not sure what it could be. Works fine in the sample thing I provided πŸ™‚

  • Malinga

    Any chance you can help me out? I’m having a lot of trouble with this IPN stuff…never used it before – this is the script but it won’t insert anything to the DB:

    $dbhost = 'x';
    $dbname = 'x';
    $dbuser = 'x';
    $dbpasswd = 'x';

    $conn = mysql_connect($dbhost, $dbuser, $dbpasswd) or die(mysql_error());
    mysql_select_db($dbname) or die(mysql_error());

    if(array_key_exists('test_ipn', $ipn_post_data) && 1 === (int) $ipn_post_data['test_ipn'])
        $url = '';
        $url = '';

    // Set up request to PayPal
    $request = curl_init();
    curl_setopt_array($request, array
        CURLOPT_URL => $url,
        CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $ipn_post_data),
        CURLOPT_CAINFO => 'cacert.pem',

    // Execute request and get response and status code
    $response = curl_exec($request);
    $status   = curl_getinfo($request, CURLINFO_HTTP_CODE);

    // Close connection

    if($status == 200 && $response == 'VERIFIED')


    mysql_query("INSERT INTO test VALUES ('$a', '$b', '$c');");

        // Not good. Ignore, or log for investigation...
    • Sure the IPN part works? Do you get what you expect? Tested the database stuff by itself? Sure it works? Just take it apart and make sure each part works.

      Also, do you have the ‘cacert.pem’ file?

  • Richard Brum

    I’m a complete noob at this… what do I do with the data once the script gets to the ‘All good! Proceed…’ part?

    • That really depends on what you are going to use it for.

  • Penton

    May I know where can I download your ipn_handler.class.php class file? thanks!

  • Blake

    I am using your sample to report my IPN transactions. The log only reports the last 10 transactions and the final two are identical. Do you know what is wrong?

    • If you use the exact same code as in my sample, you might notice that the log uses a tail function to only show the last 10 IPNs. If you want to display more than that, just… adjust the number it shows, or skip using that function.

  • Pingback: PHP: risorse su PayPal | Gabriele Romanato()

  • John Elkington

    Very clear and nicely written tutorial, – but I have problems, I’m afraid.
    I uploaded your files (listener.php and ipn_handler.class.php) to my site, and using the PayPal sandbox I was pleased to see that the IPN was successfully sent. However no log.txt file was created, and there is also no $_POST data. I tried adding a final line to the listener.php file

    mail($myEmailAddress, 'Test', 'Test message');

    just to see if this file was being executed, but even that didn’t produce an email message. Would be very grateful if you could indicate what I should try next!
    Best regards

    • Maybe check into file permissions? Might be the script tries to create a text file, isn’t allowed to, and then fails before it gets the chance to send the mail.

      (You should really learn better ways to debug than using the mail function by the way πŸ˜› )

      • John Elkington

        Hi Torleif
        Thanks for your very prompt reply.
        I wrote a short php program to test whether ‘file-put-contents($fn,$data);’ worked in this environment, and it successfully created the file. So no problems there, it would seem.
        I did note, however, that I had to give the full URL pathname of the ipn_handler.class.php file in the ‘include’ statement at the top of the listener.php file, otherwise the PayPal sandbox produced an error message saying that the URL was wrong.
        Grateful for more suggestions!

        • Aha, but that’s good. Did you get it to work now then?

          • John Elkington

            Hi Torleif
            I’m still baffled. You may raise an eyebrow at my using a ‘mail();’ command as a de-bugging aid, but it is an attempt to see where the process breaks down.
            If I include a mail(); command immediately below the include statement (the first line of ‘listener.php’), then the mail function works. If I replace the mail function immediately before the ‘date_default’ line, then the mail function doesn’t work. It’s as if there’s something in the class definition and code which is causing a problem. So I’m currently stumped. I’d be grateful for further ideas – and sorry if I’m spoiling your breakfast…

          • Haha, no breakfast spoiled here πŸ˜‰

            Have you tried to just send some post data to the IPN handler yourself? First you could even try to just visit the handler in the browser and see what happens.

  • Jarrad

    Hi there,

    When should you check your inventory, and if it is depleted, how do I get my IPN to send a message back to PayPal to abort the transaction?

    I can check inventory BEFORE sending the user to paypal, but i cannot rely on this as the person may leave paypal (i.e. if they change their mind). If I did this someone could deplete my inventory just by clicking BUY, but then leaving the paypal page over and over.

    What I need is to check the inventory AFTER the person has entered their billing info, incase more than one person is purchasing the LAST item at the same time. If they have click PAY NOW, PAYPAL sends my IPN a message, my IPN checks inventory and discovers there is none left, what should it send back to CANCEL the transaction and inform the customer no payment was made?



    • Well, this is a business issue that I can’t really answer. I think I would probably do a check before and then actually subtract after.

      If the check before fails, then it’s easy to just stop the process. If however the check fails after, then it’s a bit more of an issue. But how you handle this depends a lot on what you want to do or can do. For example, if this happens, you could just notify yourself that you need to restock. Then the order would still be good, only that the one buying it might get it a bit later because you need to get your hands on more items (which you should notify him of of course). If however you can’t get more items, then you’d have to issue a refund or something like that. IPN is a one-way service and I don’t think there is a way to cancel a transaction anyways. But there are fairly simple ways to tell PayPal to issue a refund. Never done this myself, but you can check out this question on Stack Overflow for example. When the refund has been issued, you will get a message about it sent to your IPN handler though, so then you can do some special processing of that πŸ™‚

      Another thing you might think about is if you can use PDT to check inventory even quicker, with immediate response to the user. This would work for users that return to your site right after having done the payment. Might improve the user experience as well, since they would get quicker feedback. You’d still need IPN to catch those who don’t return to your site though, and for adding more info about the transaction if you need more info stored than you get with the PDT. Either way, to sum up, this is first of all a business question that you just have to figure out of πŸ™‚

  • Santiago

    Hi, very nice tutorial. I do need this to implement on the website im working for. However, i do get confused on what exactly i need.
    I used to use PDT. but when the old paypal that it automatically redirect after payment was made. with all the changes it got me confused. any help is appreciated. all i want is that once payment is completed. I will like to send an email to me to let me know payment is completed. and also a ‘receipt’ to the buyer/customer once he complets the payment via ipn. Since there exist a posibility that he would not return to the web once payment is made.
    Any help is appreciate d
    Thanks in advance..

    • You can use them both in combination. For example use the IPN to send email to you and receipt to customer. If customer returns you could use PDT to generate the receipt at an earlier stage and just show it to them. Then IPN could see the receipt is already generated and send it as an email, or something along those lines. Really depends on what you need or want to do. A business question I would say πŸ™‚

      • Santiago

        Thank you for the quick reply. I would like to use them both why not . However, what i am still confused on how to use is IPN, i try to test it and i do get confused on the php code for what exactly i need to use. what i tried so far didnt work. when i tried your code on IPN came out with some http 400 error on my server. any idea?
        THanks again

        • Gianluca

          I too have the same problem.

          IPN delivery failed. HTTP error code 400: Bad Request
          There is something to having set the Listener?

  • Hello, your tutorials about paypal are fantastic! I used the PDT one yet.
    Now i’m trying to use the IPN but i’ve a stupid problem i think.
    Two things before continue reading:
    1) I’m little more than a neophyte;
    2) I’m italian, so i could write in a strange english πŸ˜›
    Let’s go on!
    My problem start at the STEP 1.
    So I use the Instant Payment Notification (IPN) simulator and send the IPN to the specified IPN handler URL.
    The simulator send a POST to my listener, right?
    So in my listener i’ve write:
    “$ipn_post_data = $_POST;
    foreach($ipn_post_data as $i => $a){
    echo $a.”;
    Now my listener have to show me all the values of the array items, right?
    But what I see is only a white, empty page…
    Maybe a stupid problem, but for me is a big one.
    Thanks a lot for your attention.

    • Think you misunderstand how POST and IPN works. When you tell the simulator to send the IPN, it sends a POST request to your PHP script. The IPN client then gets back a response with the values echoed out, PayPal of course ignores. When you visit the page, you don’t send it any POST data, and the script have nothing to print out to you, so you get nothing.

      The only way for you to see what the IPN client sends to your listener script is to store it somewhere. If you check out my sample, you’ll see that I just dump the POST I get into a text file. I then in a different script read out the contents of that file to show it to me.

      Hope that helps. Reading up a bit on how the web works would probably be helpful if you’re going to do web development πŸ™‚

  • Byron L.

    This is very nicely done. I had to use the cacert.pem file and related settings to get mine to work. I’m using this with the Yii framework. There is a paypal module already written for Yii, but I like your code as a better starting point. I’d like to eventually include your code in a Yii module of my own.

    I’m planning on expanding it to include drop in classes for additional functionality. For example, subscription payments. Anyway, would you have a problem with me including your code? I’d leave your header in any files included and possibly add some open source licensing.

    • I’d have no problem with that! Thank you for linking back, and please let me know once you have something useful. Maybe I can make use of it too πŸ˜‰

  • Meer

    I studied the tutorial twice but it not help me in understanding the concept of IPN.
    It was time wasting for me.

    • What do you feel is missing? I wasn’t really trying to explain the concept of IPN, but rather how to implement it using PHP. The concept of IPN is quite simple, so if you’re a developer and not able to understand the concept even… then… yeah…

  • i found your “fixing the charset” point quite useful.

    • Yeah, was annoying to get data in the wrong charset to say the least.

  • Michael

    My server is behind a proxy that I don’t have access to configure. I can use curl to connect to non-SSL websites, but SSL websites are being blocked. The result is that I can receive the IPN data from PP, but I can’t POST back to to verify. I have tried to POST back to, but it looks like the site is trying to redirect back to https. Any suggestions on how to verify the IPN data using an unencrypted system?

    As well, and this may be beyond this tutorial, what happens if I don’t verify the data? Will PP still process the order?

    My thanks.

    • Why is your server behind a proxy blocking secure connections? I’d look into that first of all. Seems like a rather dumb set up. Get someone to fix it, or move your stuff to a server which doesn’t have dumb restrictions like that.

      Don’t think you can verify without encryption, which makes sense, since this is about security and there is money involved. The IPN is done after the order is processed at PayPal, so I suppose you could just not verify the data, but you might get errors in your logs in PayPal, since they keep track of successful notifications and such. Either way, how do you know it’s legitimate if you don’t verify? Could be me faking it and fooling your system.

  • Thanks Brian for posting your problem about the status being 0 and response being null:
    my website is also on godaddy and removing the CAINFO also fixed everything!

    Most of all thanks to you Torleif for these tutorials, Paypal clearly lacks organisation in their documentation and understandable examples.

  • Kamran Khan

    You are amazing. Programmers like you inspire me to learn more and write better code. Thanks!

    • Great to hear! Hope I inspire you to try to help and inspire others as well πŸ˜‰

  • Thanks for the posts!
    I am using Flash website and when a client click on Pay by PayPal, the web page is transferred to PayPal where the client pays using PayPal ExpressCheckout. Everything works fine. PayPal IPN sends an “echo” when a payment is made.
    My question is the following.
    What field should I use to uniquely identify my transaction in the database. Before diverting to PayPal the only field I have is the “token” from PayPal?

    • Think that would be the txn_id (transaction id).

  • Chris

    This is probably the most life saving tutorial I ve ever read.
    Thanks a lot….

    Would be nice if you could also provide mysql structures for PDT and IPN tables πŸ™‚

    • Haha, yeah… well, those structures depends so much on what you’re going to do with it, so I won’t do that :p

      • Chris

        ok, I was a bit lazy asking for that πŸ™‚ I want to log all o them so …. I guess it will be better to go through documentation anyway

        • If you just want to log them, I would probably just create a table with three columns: one regular auto incrementing id, one varchar column for the transaction id (with for example a unique index), and finally a text column where I would just dump the whole IPN data array as JSON πŸ™‚

          That way all the data will be there, and you can easily process it and split it into more columns if you need to later πŸ™‚

          • Chris

            that’s good idea !! Thanks

  • Andrew

    Any one know how to install CURL on my server?

    Notice: Use of undefined constant CURLOPT

  • Chris

    kind of strange but seems like paypal not always send IPN as a POST – at least in the sandbox.
    Sometimes IPN was working – sometimes not – getting 400 bad Request ( $ipn_data===FALSE in your code)

    so I changed
    $handler = new My_Ipn_Handler();

    and now seems to be fine …

    • Well it should be POST. If it’s not, I would say there might be something fishy going on…

  • Alex

    I have a question. If you’ve managed to integrate that IPN Script into your website, wouldn’t be that PDT Script unneccessary, because the transaction details will be sent with IPN anyways?


    • That would be correct. In addition IPN gives you more details than PDT does. BUT, what you might want to do is to use them both, since you get the PDT immediately when the user returns to your site. So you could use that to show them info and/or make bought stuff available at once, rather than wait for the IPN, since the IPN is asynchronous.

      Hope that made sense πŸ™‚

      • Alex

        I see, thanks for your reply. So that means IPN doesnt call the site I defined directy after the payment?

        And does the Pending status of a payment means that this buyer has to be checked by PayPal or something? or does it mean, I’ve to wait until his money actually get’s transferred from his bank acc/cc to PayPal and then to me?

        • Well, normally it should do it pretty instantly, but you have no guarantee it will have happened before the user returns to your site.

          Exactly what the pending status means, you’ll have to look up in the manual πŸ™‚

  • PAT

    Thanks for posting this. This is a great tutorial. I like that the tutorial wasn’t too complicated to follow.

    • Mission successful πŸ˜‰

  • RJ

    Your tutorials are very easy to follow. Thanks!

  • Kamran

    Hi Torleif,

    I used this tutorial of yours eons ago to implement it on a website I’ve been working on. I just started to test it live and Paypal seems to not be doing anything whereas the sandbox works exactly how I want it to.

    I’m troubleshooting it to find out if I’ve messed up anywhere, but I was just curious to hear from you if you think this might have something to with my server not having SSL.

    In your script above, do you presume that the server where the listener is, happens to have an SSL certificate? Or is that not what I should be working to fix here?

    If so, is there a workaround to this that doesn’t require SSL to be employed on the server?

    Thank you!

    • Kamran

      Stupid mistake. I just checked my server logs and realized that the IPN is being sent to the wrong URL.

      Apologies. πŸ™‚

      • Hehe, that happens πŸ˜€

        And no, the server where the listener is shouldn’t need to have an SSL certificate as far as I know. I’m pretty sure I don’t at least πŸ™‚

    • Brian

      One of the things you have to be careful of between sandbox and live is the data sets that PP sends aren’t the same. The whole live POST array has about 55 or 60 Name-Value pairs in it, and the whole sandbox POST array has maybe 50 or 52 NVPs in it.

      My best advice on this is to identify the data that you need to work with and only reference those N-V pairs. For the purposes of my website, there are only about 25 N-V pairs that I have any use for. The benefit to this is you don’t really have to worry too much about PP changing datasets on you as they “improve” things. The other benefit is that it makes your stored procedures easier to write, and it makes it easier to pass data to your SPs from your PHP code.

      Also, beware that there are several N-V pairs that PP is in the process of deprecating. AFAIK, any N-V pair that starts with “mc_” is going to be gotten rid of. This isn’t a particular problem because, for one example, mc_fee and payment_fee hold the same data, so use payment_fee instead of mc_fee.

  • Zorinik

    Thanks man for both tuts, it’s 2 days I’m banging my head around looking for a easy way to sell stuff with PayPal. Their docs are the hell on earth xD

  • Thanks for the tutorial. I have one question. I actually require setting up two separate IPN url from my website. Is that possible?

    • What do you mean? Why do you require two IPN urls?

      You can only specify one IPN url for your PayPal account as far as I know. I think you might be able to specify IPN urls explicitly for each PayPal button though… you can try that perhaps?

  • Martin

    Hi Torleif,

    Thanks for this excellent tutorial!

    Quick question – I’m writing the txn_id to a database (along with other info, e.g. payer_email and item_number). What’s the correct code to use? I was thinking something along the lines of:

    $txn_id = $ipn_post_data['txn_id'];
    mysql_query("INSERT INTO sales VALUES('', '$payer_email', '$item_number', '$txn_id', CURRENT_TIMESTAMP, '', 'active')");

    … but that didn’t seem to do the trick (an entry is being created in the database, it’s just there’s no data!)


    • Well, first of all you should never ever just insert text into a query like that unless you have full control over what it is. Use mysql_real_escape_string when doing that. See the examples on the manual page.

      Otherwise, have you tried to see what is really in those variables? I see you get the $txn_id from the $ipn_post_data array, but none of the others. Sure they are actually set to something?

      • Martin

        Oh yeah, sure – I missed that in my haste!
        RE the other variables – I have used similar code to $tkn_id but hadn’t included them in the comment above.
        It sounds like that code is correct though?
        Once again, thanks for the excellent tutorial. It’s so much easier to follow than the others I’ve come across!

        • Martin

          Ah, wrong column types in database… schoolboy error!

          • Hehe, been there! Good to hear you got it working. An easier to follow tutorial was my goal in writing this, so good to hear that I succeeded πŸ˜€

  • Zac

    I was using a piece of sample code from stack overflow before, and I was running into issues with the SSL Certificate and cURL. This is a cleaner piece of code and works right away. Thanks!

  • Pingback: Zac's Attic Blog » cURL and PayPal IPN()

  • Wonderful guide. I’m definitely using this as a base to my current project. Thank you very much!

  • brian

    I hate when people say ‘this is so easy’ and then it doesn’t work – makes me so stupid and insulted.

    I understand the IPN concept, and the conversation between client, server and PayPal, and the difference between http and https. My problem is getting *anything* at all from the simulator. My handler url is accessible from the rest of the world and i have debug statements and log write lines all over the code, but asking PayPal to simulate an IPN call always results in the same error message “IPN delivery failed. Unable to connect to the specified URL. Please verify the URL and try again.” despite sometimes hitting my handler page and sometimes not.

    The simulator tool seems to be as crappy as PayPal’s documentation.

    • First of all I’d just like to say that the only one able to make a person stupid and insulted is really himself πŸ˜‰ No reason to feel insulted just because someone has gotten something to work which you haven’t. It’s just a matter of time and perseverance πŸ™‚

      The simulator tool worked totally fine when I was testing, and it was quite simple to use. Are you sure the URL you give it is correct? Have you tried to visit the URL in your browser to see if it exists and that it doesn’t throw any errors or anything like that? Checked the domain through for example to see if it is accessible from other places than your machine? What do you do with the debug statements and log write lines? Remember you need to store them in a file or something and not just echo them out. And if you do store them in a file, have you made sure this actually works?

      I have no idea what’s wrong with your code, but there are some thoughts at least.. Good luck πŸ™‚

  • Igor Borodin

    First, I’d certainly like to join the chorus of grateful users of your tutorial. Thank you indeed. And, inevitably, a question:

    I need to ‘link’ a confirmed payment to particular user who initiated the payment. In order to do this I wanted to use passthrough variable ‘item_number’. So, in the PayPal ‘Buy Now’ button I do added a line:

    <input="hidden" name="item_number" value="abc123">

    Yet, the IPN $_POST array shows the key – item_number, but it’s empty.
    What am I missing? Is it possible that passthrough variables are NOT returned if button is encrypted? Or could you suggest other ways of matching a payer and a payment confirmed in IPN?
    Thank you once again,
    Igor Borodin

    • Igor Borodin

      I didn’t realize that I have to use html entities. So, here is the line added to the PayPal button form:
      < input=”hidden” name=”item_number” value=”abc123″ >

      • Igor Borodin

        Yeah, it’s too late: I missed the word ‘type’

    • Fixed your comment. You just need to use the <code> tag πŸ™‚ Did you get it to work though? As far as I can see, the item_number field should work fine. Used it myself when I was working with this.

  • Pingback: Desarrollo web Γ‘gil con Symfony2, dos meses despuΓ©s |

  • AYCDesign

    Ok, I am not really good at this debugging thing, I am getting

    IPN delivery failed. HTTP error code 400: Bad Request

    in the Instant Payment Notification (IPN) simulator.

    I go to the URL directly, and just a blank screen. Where do I start?

    • Try adding some logging to a file in your handler. Try the simulator again, and then check your logfile.

    • Luis

      I had this problem too. Then i added the full path to the class file in the include on listener.php – after this, the simulator returned a success message, but now my listener is not able to include the file anymore… But that is a local problem, so i guess i does not have a thing to do with the tutorial perse

  • Great job Torleif!!!
    I am a newbie to php.
    Your tutorial has really helped me to understand IPN and I am very happy that you take out time to reply to people. Taking your tutorial as base for my IPN I tried very hard but didn’t succeed to fullest. Once IPN data is verified I need to save data in database and send access code to user via email. But I am not able to find out why it is not working can you please look at the code and tell me where am I going wrong. I want to deploy this for production at earliest, so need to make working. Waiting for your reply.
    here is my IPN code:

     $value) {
    $value = urlencode(stripslashes($value));
    $req .= "&amp;$key=$value";

    // 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($req) . "\r\n\r\n";
    $fp = fsockopen ('ssl://', 443, $errno, $errstr, 30);

    // assign posted variables to local variables
    $item_name = $_POST['item_name'];
    $item_number = $_POST['item_number'];
    $payment_status = $_POST['payment_status'];
    $payment_amount = $_POST['mc_gross'];
    $payment_currency = $_POST['mc_currency'];
    $txn_id = $_POST['txn_id'];
    $receiver_email = $_POST['receiver_email'];
    $payer_email = $_POST['payer_email'];

    if (!$fp)
        print("Hey Likhit there is some error");
        fputs ($fp, $header . $req);
        while (!feof($fp))
            $res = fgets ($fp, 1024);
            if (strcmp ($res, "VERIFIED") == 0)
                $user=new user();
                    $img_url ="";
                    $page_status =$user-&gt;error_msg;
                    $title="Access Code";
            else if (strcmp ($res, "INVALID") == 0)
        // log for manual investigation
        fclose ($fp);
    function randomPrefix($length)
            //random number function
            return $random;
        function send_email($to,$title,$message)
                global $user;
                global $class;
                global $page_status;
                global $img_url;
                $code_expiry=date("Y-m-d", mktime(date("H"),date("i"),date("s"),date("m"),date("d")+1,date("Y")));
                $SMTP_FROM = "xyz@pqr,com";
                $SMTP_SERVER = "xyz";
                $SMTP_PORT = "543";
                $SSL_ENABLE = "1";
                $SMTP_USER = "";
                $SMTP_PASSWORD = "mind#chips#";
                $from=$SMTP_FROM;                           /* Change this to your address like ""; */ $sender_line=__LINE__;
                /* Change this to your test recipient address */ $recipient_line=__LINE__;
                $subject = $title;
                $myFile = "email/email.template";

                $fh = fopen($myFile, 'r');
                $body = fread($fh, filesize($myFile));
                $url = "";

                $body ='';
                $body = $body."Dear User ";
                $body = $body."Your access Code is ".$message." and this code will be valid upto ".$code_expiry."";
                $body = $body."Thanks &amp; Regards, ";
                $body = $body."________________________________________";
                $body = $body."<a></a>";
                $body = $body."";

                        die("Please set the messages sender address in line ".$sender_line." of the script ".basename(__FILE__)."\n");
                        die("Please set the messages recipient address in line ".$recipient_line." of the script ".basename(__FILE__)."\n");
                $smtp=new smtp_class;
                $smtp-&gt;socks_host_name = '';    
                $smtp-&gt;socks_host_port = 1080;  
                $smtp-&gt;socks_version = '5';
                                * If possible specify in this array the address of at least on local
                                * DNS that may be queried from your network.

                        * If GetMXRR function is available but it is not functional, to use
                        * the direct delivery mode, you may use a replacement function.


                global  $activation_message;
                                "From: $from",
                                "To: $to",
                                "Subject: $subject",
                                "Date: ".strftime("%a, %d %b %Y %H:%M:%S %Z"),
                                "Content-type: text/html\r\n"
                      $img_url ="";
                      $page_status = "Email has been sent to your email id <b><i>".$to."</i></b>.";
                      $user-&gt;access_code = $message;

                      $img_url ="";
                      $page_status =  "Problem in sending mail.";

    Thanks a ton in Advance πŸ™‚ !!!

    • Hey, no offense, but your code is quite a mess, and I don’t think I feel like debugging it for you when it has nothing to do with the post here πŸ™‚

      I will, however, recommend that you use Swiftmailer instead of what crazy thing you’re trying to do. If you still can’t figure out what’s wrong, you can always try

    • Brian

      Likhit, there’s a much easier email function in PHP.
      $email = “”;
      $subject = “Important Message”;
      $msg = “this is your message body”;
      $headers = “From:” . “” . “X-mailer: PHP/”.phpversion();
      mail ($email, $subject, $msg, $headers);

      all of that other stuff needs to be set in the (if you’re using sendmail) or in (if you’re using postfix)

      • I’d much rather use Swiftmailer πŸ™‚

  • alessandro

    Thanks man !!!!
    Your sample are PERFECT ! I was looking for something like this about 2 days !

    Thank you !

  • Well, i have been working on Paypal IPN.
    I need some information relate to Recuring proccess, please help me in it.

    Step 1 > Client Subscribe Package 1 [ $200 ]/ month
    Step 2 > Redirected to Paypal
    Step 3 > Payment and Agreement , Recurnig Profile Created in Users Paypal Account
    Step 4 > IPN Response To My Website for Success Lead
    Step 5 > Client Back to my website.

    Question :
    1.Next Month Paypal will Automatically Charge to Client with Recuring will paypal will return response IPN?.
    2.If client want to switch to new package, should he have to remove the old profile of recuring from Paypal Account.

    Hope for positive response.

    Best Regards
    Taha Ali Adil

    • Sorry, but these are questions you’ll have to ask PayPal about or find in their documentation yourself. Never used recurring payments before.

  • Brian

    OMG, this thing was working for me for about 6 months, now all of a sudden it isn’t. I can’t get an answer out of PP, but has their key changed recently? The public key I have is good until 2036.

    My original problem was the path to the certificate. The certificate is in the same directory as the script that has this code in it, so my path is good, the cert name is right, but it bombed on the status == 200 and the response == ‘VERIFIED’ line again.

    I really do not like PP.

    • No idea. Haven’t been touching PayPal code for over a year now πŸ™‚

  • Dave

    Hi Torleif,

    I could really use your help as I have been searching for an answer to this question/problem for a few days now.

    I am using cURL to gather an HTML page over https and I am returned the code 200 but when viewing the page it is incomplete with the text “please wait”. It appears cURL believes everything is good and returning early.

    Any ideas?

    • Does the page you’re trying to retrieve work through your browser, or do you get “please wait” there as well? Does the curl_error function give you anything?

  • Sarah

    Can any one tell me how i store the value of when i return in ipn? i did try but all the time fail, here is my code of ipn to store the value in db

    use_sandbox = true;
    $txn_id = mysql_real_escape_string($_POST['txn_id']);
    $item_number= mysql_real_escape_string($_POST['item_number']);
    $payer_email = mysql_real_escape_string($_POST['payer_email']);
    $mc_gross = mysql_real_escape_string($_POST['mc_gross']);
    $sql = "INSERT INTO orders VALUES (NULL, '$txn_id', '$payer_email','$mc_gross','$item_number');";

    if (!mysql_query($sql)) {
    • Not without any error messages or anything. Either way, I’d recommend you go to Stack Overflow and ask there instead, since general db usage isn’t really the subject of this blog post.

  • Tim

    I’ve been scratching my head over this one for the whole day now. I’ve tried all error checking and i still can’t come up with an answer.

    I have implemented all of your code above but when it comes to getting the code form $request , the variable is blank. However, i am still getting values in $response (VERIFIED) and $status (200). So you can imagine my confusion with this. And the variable $ipn_post_data has the relevant array from the $_POST. Also curl_error() is sending back nothing so it appears there is no error there. I’ve also turned off the ssl verifier just in case and still no luck.

    Any ideas as to what may be wrong? and any possible solutions?

  • Heather

    I get this error:
    Warning: strftime() [function.strftime]: It is not safe to rely on the system’s timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected ‘Asia/Hong_Kong’ for ‘HKT/8.0/no DST’ instead in…

    The line affected goes like:
    // put header at the top of the compiled template
    $template_header = “_version.”, created on “.strftime(“%Y-%m-%d %H:%M:%S”).”\n”;
    $template_header .= ” compiled from “.strtr(urlencode($resource_name), array(‘%2F’=>’/’, ‘%3A’=>’:’)).” */ ?>\n”;

    Any idea how to change it to?

    Thanks a lot

  • Paypal should erase their tech site completely and substitute for your article. Thank you very much!

  • prashant

    simple and nice tutorial

  • Vikram

    I was working on Paypal for last 10 days but to no avail. Suddenly across you post today. After little tweaking your code a little bit, was able to complete the integration.

    Thanks a ton Torleif,

    Awesome Post !

  • superrider

    I found a great article while googling, i recommend it.

  • Kapil

    This is not working.
    I setup a page “ipn_handler.class.php” in a folder.
    Then included in index.php, and when I am clicking on Send IPN button in Paypal. It’s saying “IPN delivery failed. HTTP error code 400: Bad Request “.

    What shall I do?

    • Kapil

      Moreover, when I call the index.php directly, it is writing in error_log that “[28-Apr-2012 10:00:28] PHP Warning: array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object in /home/danny/site/payments/index.php on line 49

    • You should find out why it’s giving a 400. About the error written to the log, you should really be able to figure it out quite easily. It says right there what’s wrong πŸ™‚

  • deniele

    Help…, I have some problem when combining PDT and IPN in one domain .
    When i used PDT and IPN separately, on different domain , they work fine,
    but when i join them together, such as:
    Then, only PDT/index.php can get well response from the paypal,
    But, IPN/listener.php , don’t receive anything, why ??? Wait for a few days, still no response.
    actually, i don’t know what happens.
    can someone help me ?

    • Maybe because you’ve forgotten to change the configuration of your PayPal account properly? I don’t know. I have both an IPN and a PDT example on the same domain, shouldn’t be any issues as long as PayPal is configured correctly and you’re code is correct.

  • deniele

    hii torleif,

    1. I have checked for the configuration,
    a. for IPN, i just did this setting,
    – URL :
    – Receive IPN messages (Enabled)
    b. for PDT, i just did this setting,
    – Auto Return:On
    – Return URL :
    – Payment Data Transfer : On

    is that correct ? and is there any others setting that must be configured on paypal ?

    2. Then, i check whether, the problems is coming from the code ?
    by using Instant Payment Notification (IPN) Simulator,

    A. first try, only IPN code in a domain,
    on listener.php , i just put 2 lines of code, such as :
    $output = json_encode($_POST);
    file_put_contents(‘log.txt’, $output.PHP_EOL, FILE_APPEND);

    Then, post data from paypal is received and saved properly on log.txt => success

    B. Then, I put IPN and PDT code together (which i describe on previously comment) .
    B.1 Using Buy Now buttons
    PDT code, work fine..
    but IPN, received nothing…

    B.2 Using Instant Payment Notification (IPN) simulator

    on simpleListener.php.php (IPN), i just put 2 lines of code ( same like above),
    after send IPN,
    Then, it looks more better then above , Because log.txt is created,
    ( simpleListener.php should be received the post data from paypal , good news)
    oh my god, inside log.txt only showed => [ ] ( like array symbol)

    I delete again the log.txt and try to send IPN again, the results also the same .

    I am very confusing, is that possible ? wrong with the code if this condition is happening ?
    or something wrong with the hosting ? Because i am using free hosting with the domain free as well .

    is there any better solution for… πŸ™

    • Maybe check your button code, as I think you can override both PDT and IPN settings there. Make sure you’re using the correct account both places, correct tokens, etc. Other than that I really have no idea. Sorry :S

      • deniele

        ok, i will check again for other code,
        and Most of all, thank you so much…Torleif.. for your best tutorial .


  • This tutorial has helped me save a lot of time. It has helped me to complete my project quickly.

    Thank you very much for sharing this tutorial and the code.


  • Aryeh

    is there any way to download all the code you are using in a zip file — it will be much more simple

    i do not understand how you are getting all the data – i will probably understand if i see the code.


    • All you need is ipn_handler.class.php and listener.php. You can view the source of both on the sample page. The listener dumps data in a log file, and the rest of the stuff is just to display it. You can view the source of that too, but it’s not needed for the IPN listener to work.

  • Adam

    Thanks, first of all, for probably one of the best tutorials on the internet. It works on my site almost perfectly. There seems to be a problem with the IPN handler class when $post_data contains single quotes (‘) in its fields. Strangely, it seems to work with your listener sandbox, but on my site the IPN delivery fails with error 400.

    The single quotes are only in the data passed by Paypal (the payment details), as the user input is passed through htmlspecialchars(,ENT_QUOTES) before being stored on the database.

    Any advice would be greatly appreciated!

    • I had the same problem. It occurs if magic quotes is enabled on your server which automatically adds slashes to incoming data (so β€˜ becomes \’)

      To work correctly just insert the code below into the ipn_handler.class.php just before you β€œ// Set up request to PayPal” in the validate function. (ie put it just before all the cURL stuff).

      Hope this helps.

      // Remove quotes
      if(function_exists(β€˜get_magic_quotes_gpc’)) {
      $get_magic_quotes_exists = true;
      // Handle escape characters, which depends on setting of magic quotes
      foreach ($ipn_post_data as $key => $value) {
      if($get_magic_quotes_exists == true AND get_magic_quotes_gpc() == 1) {
      $value = stripslashes($value);
      $ipn_post_data[$key] = $value;
      • Great tip! Thanks for pitching in πŸ™‚

  • Almar

    Thanks for this very helpful tutorial. It’s very simple and very easy to use.

  • Prince

    I needed this info on May 31, 2012. Good timing!

  • Worth noting that if you’re IPN handler is listening on a port different from the default one (80), it won’t receive any notifications from the IPN servers. For example: one of my dev servers was operating at 8080.

    I set my IPN handler URL to: “” and PayPal kept telling me it can’t connect to that server. I rebooted the server, setting it to run on port 80, and changed the IPN handler URL to “” – this time PayPal sent me the notifications without a problem.

    • So PayPal doesn’t allow specifying the port number? That’s a bit annoying… luckily not very normal to have a public website listening on anything else than 80 though.

  • Malcolm

    Hi there, this is exactly what I need for our car club which we moved to vBulletin.
    I’ve taken the two source files and put them into our root where other PHP and html files are. However when I do a sandbox test, I get the following: IPN delivery failed. HTTP error code 400: Bad Request

    Just to see what would happen, I put in the URL of one of the article pages and did the test which was successful! for the URL I’m using:

    Any clues of why this is happening?


    • Sorry, no :/

    • Conor O’Leary

      I had this problem. I fixed it by commenting out the following lines in ipn_handler.class.php:
      //CURLOPT_CAINFO => ‘cacert.pem’,

  • Paul

    I like the tutorial, although I would love it if you had it as a download. I found your and it apparently builds on this code, although it is way more complex (and probably more secure).

    • Hi! The only difference from this post and that code is that the code is wrapped in a class and some methods. Should be well commented and easy to follow. You’ll find the code in this post more or less untouched within that class πŸ™‚

    • It’s not way more complex. It’s actually pretty much the same code if you compare it. Just split up into som functions and put in a class. It’s pretty straight forward really. If you find it terribly complex I’d suggest reading up a bit more on PHP basics πŸ™‚

  • ejv1233

    is there a way to create an ipn response from other than paypal? I have a template that has PayPal ipn integrated into it, Which I dont want. It would be much easier to just mimic papals response of “verified” and “cpmpleted”transaction to get the template to add info to mydatabase. Can I redirect the curl to a php page hosted on the same server as my site that performs just like paypals notifier?

    • No, the IPN is PayPal specific, and one of the steps is to call back to PayPal to confirm that it is a legitimate notification from PayPal. As this is about payments and money, this is important.

      If you just want to update your database with some information upon some different event, then just… update your database like you normally would. If you’re not using PayPal, don’t use PayPal.

  • Goon

    PayPal’s IPN on sandbox has stopped calling my IPN listener since yesterday. Although I have not modified the code within the listener, I added a single-line mail call at the top of the code which is supposed to output the post array within the body of the mail, but still nothing. This has also been confirmed by checking my Apache logs and checking the IPN history from within PayPal. Further evidence that this is a PayPal issue is that when I perform a manual IPN call using the simulator, everything works fine – no errors at all and the order gets marked as usual (and I receive the email with the post variables), so I definitely know it’s not the listener at fault. Not sure if anyone else has experienced this?

  • Alwyn

    I want to set up my website with payment with is there someone can help me please.

  • Gary jones

    Where should my code for storing some of the data into a database go, in the handler or the listener? and in which bit?

    • gary

      I put my db routine at the bottom of the listener getting the info using $_POST i.e. $_POST[‘item_name’], I dont know if thats a good place but seems to work.

      • You should use the $data array, where I do file_put_contents. The $data array is basically what’s in $_POST, but it has been converted to the correct encoding and such.

        • gary

          Thanks Torleif I will do that and thanks for the great tutorial.
          I did try using the $data array and leaving in the log as well but it wasnt working for me.

          • Might be something else going on. Make sure the $data array actually contains something. If it doesn’t, if the validation fails, try to check that the curl request in the handler actually gets a response. Just edit the handler and dump the response to a file and see what’s there. If it’s empty try use the curl_error function to see if curl is failing for some reason.

          • Gary

            I forgot to set my GLOBALS duh, I am glad to report it is working great now.

    • Well, if you check out my example at the very end of the post, you would do it in place of my file_put_contents.

  • Marc Sailor

    Thanks, I used for several years the code from paypal (fsockopen…) and yesterday starts to fail without a reason.
    Now I changed the code with yours and it’s OK!
    Even without the ssl ca.

  • Gary

    At the moment I am not using the script for log files but just saving onto my server and accessing the txt file from a browser. Whats the best way of securing my log.txt so no one else can access it via browser or robot.

    • Gary

      At the moment in the directory on the server i have an .httaccess with

      deny from all

      which stops access via the browser but allows me to still write to the log.txt and access it via plesk, is this enough protection when the log.txt has so much sensitive data.

      • Gary


        deny from all

      • Gary

        no =”” in the above!

      • I wouldn’t really know. I’d probably try to store it in a database or something instead of a plain text file though, but depends on how you’re using it I suppose. Maybe ask on an appropriate site.

  • Oshirowanen

    This doesn’t work for me, I keep getting invalid. Check this out:

    • Check the response you get for error messages. If it’s empty, use the curl_error function.

    • Hi Oshirowanen, try to add this code
      if(curl_exec($request) === false){echo ‘Curl Error :’.curl_error($request);;} below $response = curl_exec($request); It’s work for me.

  • Pingback: Paypal SandBox IPN always returns INVALID()

  • scottrichardson

    Using your class and listener.php file and testing with the IPN simulator, but I keep getting “IPN delivery failed. HTTP error code 400: Bad Request”. Testing using the cart checkout, instant and verified. If I remove the if($data === FALSE) part of the listener, I see it DOES write to the log file, but only something like “1345529280 false”.

    Any help is greatly appreciated – I’d like to use this setup to update an order in a database to set it’s status to ‘paid’.

    • scottrichardson

      I literally copied your source code – works when I test using your URL, but not mine. Go figure.

      • Since you literally copied everything, did you remember to swap out email addresses and such? And did you set up PayPal correctly?

        • scottrichardson

          Yeah I did all that. I ended up using the script found at Paypal’s X web site, which worked for me.

  • DanRomanchik

    If I post some form data to PayPal along with whatever form fields are part of the Buy Now button form, will PayPal pass those through to me?

    • Not that I’m aware of. But there might be some special fields meant for custom data. Check out the spec.

  • pvanthony

    I have a customer with the paypal customer first name Pat D’Silva. When he purchase, paypal accepts the payment but the ipn is not working. I am guessing that the apostrophe in the name is causing the problem.

    Is my conclusion correct? Has any test been done with apostrophe in the first or last name in paypal account with regards to the ipn?

    Really need advice on this.

  • pvanthony

    Did more testing. This time I just used the code from the bottom of the page at Then I went to the Instant Payment Notification (IPN) simulator to try it out.

    First name without apostrophe works well. Frist names with apostrophe fails.

    I wonder what I am doing wrong. It is working great on your site, But not when I am doing it on my site. I really wonder what is wrong with apostrophe.

    I hope you have the time to advise me.


  • Nicole

    Sounds like you just need to escape the apostrophe?

    • pvanthony

      Thank you for the tip. How do I do that? Please share the method.

      • Nicole

        You could run addslashes function on all the data

        • pvanthony

          The above advice on using addslashes helped me a lot. Saved me much time. Thank you very much.

          Here is what was done.

          1. With the above advice of addslashes, I checked my setting for magic_quotes_gpc and found that it was set to ON. Then I change the setting to OFF.

          2.Then the IPN script started to work even with first names with apostrophe.

          3.My next problem was the data was not going into the database. Then I added the addslashes function to the data like so,
          $posted_paypal_customer_first_name = addslashes($data[first_name]);
          $posted_paypal_customer_last_name = addslashes($data[last_name]);
          And now the data goes into the database.
          Hope this helps others.
          Please comment if there are mistakes or better ways to do this.
          Once again thank you for sharing.

          • If you’re using MySQL you should use the mysql_real_escape function instead of addslashes. And remember, never put user-contributed data in your database without escaping it first (if it’s not strings, make sure the data is a 100% valid date/integer/whatever instead).

  • Mucho gracias for this tutorial! Both for the use of Curl and for indirectly explaining that my Web host lacked the needed certificate.

    • Ah, good to hear I’m not the only one who met that issue. Have gotten the impression that my certificate fix has caused confusion for a lot of folks :p

  • substring

    Hi There!

    Thanks for your tutorial. It’s very clear and easy to follow.
    I’m just wondering how come I’m not getting anything when I var_dump $_POST?
    my listener.php is the following


    this IPN should be able to work alone without implementing your previous PDT tutorial, right?
    (I just want to catch transaction details. I don’t need to greet customers upong their retunrs. )

    Thank you!!

    • This is happening server-to-server, so you won’t see anything you print out. To check the result you need to store it in a file or database or whatever.

  • It will only work if you don’t convert the encoding to UTF-8, but just leave it exactly like Paypal has sent it. Anyway, thanks for this tutorial!

    “Ensure that you are encoding your response string and are using the same character encoding as the original message.” from

    • Yep, that’s why I do all the cleaning up stuff after I have run the check πŸ™‚

  • Janet Chu

    MY Paypal IPN code is executed only when payments are made without checking out from the wp e-commerce shopping cart. If payments are made directly through paypal, then my IPN works fine.
    However, if payments are made through the wp e-commerce checkouts then my IPN code will not get executed at all.
    In my wp setting, I’ve made sure the followings are done: my API username, password and signature. IPN is enabled.
    In my paypal account, I’ve made sure the followings are done: IPN URL is set and IPN is enabled.
    Does anyone have the same problem? I’ve done a lot of research and I’m still not able to solve this problem. I don’t have “Bad Behaviour Plugin” which is suggested by some as the reason to this blocking of execution.
    Thank you!!

    • I’m not familiar with this plugin you’re talking about. Have you checked the PayPal code it produces? Maybe the PayPal form it creates overrides the IPN settings somehow?

  • staple

    I followed your PDT tutorial and it worked perfectly. However IPN url doesn’t seemed to get called. I’ve done IPN settings in profile and can see the ipn history and it say the messages were sent. My ipn file just write the current date time to the database. So its straight forward and it works when I try it using the web browser. But it never get called by paypal after transactions. Any idea? I use http instead https, is that an issue?

    • Don’t know for sure, but I think you should be using HTTPS for the paypal url. For your listener it shouldn’t matter. My sample listener script is not on HTTPS and seems to work fine. Does it work if you use the PayPal IPN simulator?

  • Agucho

    Great stuff! Well written, short and to the point. Loved it. And it works, which is a big plus! xD

  • dasharath

    thank u

  • TharinduDG

    Is there a way that I can send some data to the customer via the IPN ? It says that the values under the ‘custom’ variable are never presented to the customer.
    My intention is to pass some sensitive information to the customer after he/she had made a payment.
    Please Help.
    Thank You πŸ™‚

    • Then I would assume you have that sensitive information in a database or something? And if so, just wait for the IPN notification coming in which says the payment is done, and then email the info to them, unlock a page with the info, or whatever really.

  • This was important for me to understand the Ipn and used few hours to make mine work. Thanks!

  • Emanuel Olsson

    Hello! I think this is the most useful article I found so far on the topic. Heres my question, how do I use your solution and IPN / PDT together? It would be awesome with some step-by-step! I have a webshop where I want to redirect my cutomers after a purchase til something of a thank you page with a receipt of what they bought and an auto-generated mail sent. I have got it t work somehow: after a purchase im taken to the url specified an in the address field I get the transaction details like this: but how do I just take the tx and print it to the screen? Now I just get a blank screen with that POST message.
    I have a thanks.php looking like this: $value)


    if($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1)


    $value = urlencode(stripslashes($value));




    $value = urlencode($value);


    $req .= “&$key=$value”;


    // post back to paypal to validate

    $header .= “POST /cgi-bin/webscr HTTP/1.0rn”;

    $header .= “Content-Type: application/x-www-form-urlencodedrn”;

    $header .= “Content-Length: ” . strlen($req) . “rnrn”;

    // here you can use ssl, or not

    // $fp = fsockopen (‘ssl://’, 443, $errno, $errstr, 30);

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

    // process validation from paypal

    // TODO: This sample does not test the http response code.

    // All HTTP response codes must be handles or you should use an HTTP

    // library, such as cUrl.

    if (!$fp)







    fputs ($fp, $header . $req);

    while (!feof($fp))


    $res = fgets ($fp, 1024);

    if (strcmp ($res, “VERIFIED”) == 0)


    // get variables from the paypal post to us

    // see these pages for possible variables:



    // payment info

    $payment_status = $_POST[‘payment_status’];

    $payment_amount = $_POST[‘mc_gross’];

    $payment_currency = $_POST[‘mc_currency’];

    $txn_id = $_POST[‘txn_id’];

    // product info

    $item_name = $_POST[‘item_name’];

    $item_number = $_POST[‘item_number’];

    // buyer info

    $payer_email = $_POST[‘payer_email’];

    $first_name = $_POST[‘first_name’];

    $last_name = $_POST[‘last_name’];

    $address_city = $_POST[‘address_city’];

    $address_state = $_POST[‘address_state’];

    $address_country = $_POST[‘address_country’];

    // receiver_email, that’s our email address

    $receiver_email = $_POST[‘receiver_email’];

    // put your actual email address here

    $our_email = ‘’;

    // if all these conditions are true, send the email

    // note: should also verify that $txn_id was not used before

    if (($payment_status == ‘Completed’) &&

    ($receiver_email == $our_email) &&

    ($payment_amount == $amount_they_should_have_paid ) &&

    ($payment_currency == “USD”))


    foreach ($_POST as $key => $value)


    $emailtext .= $key . ” = ” .$value .”nn”;


    mail($payer_email, “Live-VERIFIED IPN”, $emailtext . “nn” . $req);



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


    // If ‘INVALID’, send an email. TODO: Log for manual investigation.

    foreach ($_POST as $key => $value)


    $emailtext .= $key . ” = ” .$value .”nn”;


    mail($payer_email, “Live-INVALID IPN”, $emailtext . “nn” . $req);




    fclose ($fp);


    • To use both, you simply have to use both. But remember that IPN and PDT works completely separate. IPN always happens. PDT only happens if the user actually returns from the PayPal process. How you handle each case depends completely on your use cases and what you’re trying to solve, so can’t really help you much there.

  • jose

    It is useful. Thanks.

  • Emanuel Olsson

    Hello again!
    I am experiencing some major issues with going live with IPN and PDT. And im getting real frustrated by Paypals non-existent support which takes at least a week and often is completely useless information. I was wondering if you had some time over and could have a look at my files and maybe see what I am doing wrong? I have def more trust in you than Paypals “customersupport”. This is my first real project and im a very new developer and it would really mean a great deal to me! The problem is that in sandbox mode everything works flawlessly, but in live it doesnt. This is how I set it up: after a user made a purchase hes sent to my pdt.php where he gets a receipt and downloadable content is shown. The ipn script is also kicking in and sending a mail confirmation with some details to both seller and buyer. Now, in sandbox mode the PDT shows all downloadabl content that is bought and the tx-token thats is sent and is in the URL ends with something like item_number=8 or so and this variable is used to show downloadable content. The ipn is sending the mails too and in it item_name is is used to show what products thst were bought with no problem. When switching to live, the user is still sent to pdt.php and SOME data is shown, but the tx-token ends with item_number= and thats it, no number which makes the downloadable content not to show either since all variables is not sent somehow. This also makes the IPN mails not completed, when some of the variables is not known and is blank.

    Here are the files and I think it might have somthing to do with the postback but im not sure:

    • Looked at it quickly, but can’t really say what’s wrong. Not sure what you mean by the tx-token ending with item_number. The tx token should just be a transaction id, it shouldn’t contain anything else. There might be more $_GET variables though, but you just ignore them. Use the tx id to get the data directly from PayPal through the curl call.

      Also, you seriously need to look into cleaning up your code… Your PDT script is pretty horrible when it comes to duplicating code and mixing html with php and so on.

      • Emanuel Olsson

        What I mean is when the user is sent to the PDT.php page a long string is sent with the URL in the address field. When in Sandbox it has values for all variabls in the string and ends with item_number=SOMENUMBER but it IS set and has value. When in Live some of the variables is missing out, or thye dont have values set, so then the url-string ends with item_name= without a number which causes the effects of not showing content and so-on. This is the same problem with the IPN. Maybe im doing the postback wrong since I dont get all values and in sandbox mode things are already setup in a way so that doesnt happen.
        Thanks for your answer too, it is appreciated. The PDT is like that since I want to show a receipt of the purchase direct and then send another with IPN. Am I thinking wrong doing this?

        • You should not use any of the variables in the URL in the address field. These can easily be changed. You should only use the transaction id, and get the actual information directly from PayPal.

          What I mean by messy is, why do you for example do:

          $h1 = 0;
          if ($item_number.$idx == '1' )
                 $h1 = 1;
          if ($h1 == '1' )
                  echo ("<div class='abouttext2'><a href='/wp-content/themes/blank/dl/ART/Gladje/art-Gladje.rar'>GlΓ€dje</a></div>n");          
                  echo ("<br />");

          Instead of just:

          if ($item_number.$idx == '1' )
                  echo ("<div class='abouttext2'><a href='/wp-content/themes/blank/dl/ART/Gladje/art-Gladje.rar'>GlΓ€dje</a></div>n");          
                  echo ("<br />");

          You also have a lot of repeated HTML which should be fairly easy to generate if you just had the content URLs and item texts in an array or something which you could map to your idx.

          You said this was your first, so don’t mean to be mean here, just that you should look into learning how to write your code a bit cleaner πŸ™‚

          • Emanuel Olsson

            You are so right and I appreciate it, I want to learn! πŸ™‚ That code I really dont know how I made up but I thought it was excellent at time… I think i thought that if there were several products/several item_number’s then only the last number would actually display a product since the idx would have changed (im using a loop) and therefore I reasoned that it would be better to save that into another variable so it didnt get lost, but I will try to reduce that for sure!

            What I dont quite get is this: you say that I shouldn’t use any of the variables in the URL, but isnt that what I get from PayPal? – all details about the purchase as a long string appended to the url in the address field when redrirected at the pdt.php? Have I tried to use wrong variables? But if so, why does they work in sandbox? πŸ™‚ And that url, isnt that THE transaction id which is sent from PayPal…? So I get it directly from them? Im sorry if im bugging you. I feel stupid.

          • Check out I try to explain it there. If things are still unclear, try

          • Emanuel Olsson

            I finally understood what you meant by not using the variables directly from the URL. I have modified my scripts so they work as yours (VERIFED & SUCCESS) and I have also cleaned it up as you mentioned. Still, the problem do still exist so it wasnt bound to the using of variables directly from the URL address field, but must be something else. And still in sandbox it works as a charm… Are you a freelancer in that way that I could pay you for have a deeper look into this?

          • No, sorry. I don’t have time or resources for that. Try outputting what you get back from PayPal and make sure it’s not an error or something. Other than that, I can’t really help you more.

          • Emanuel Olsson

            Thanks anyway! A lot! I actually got it to work after 2 days of INTENSE code reworking and debugging. Never been so happy in my life πŸ™‚

          • Good to hear. Thats how to learn πŸ™‚

  • alecarg

    Much appreciated, this has been very useful. Special thanks for being so clear about the whole process, it was trivial to follow :).

  • Akash Vyas

    Much appreciated… Good job

  • munadel

    thank you

  • inspirehappy

    Hi Torleif,

    I’m using mailchimp, who has provided me with the url to send IPN notification to, but I don’t want to use the paypal IPN default setting. I want to code individual buttons because I’m using one paypal account for various sales. Anyhow I need some coding to put into the “buy” button- per paypal IPN guidebook : (see page 25) Do you have any suggestions? BTW I know nothing of coding, so a copy and paste solution would be best =)

    Thank you in advance for your help.

    • Try using the button factory thing that PayPal offers perhaps. I assume you’ll be able to set the IPN explicitly there? If not… learn how to code? :p

  • finrod


    just out of curiosity: the documentation ( says

    3. Your listener returns an empty HTTP 200 response.
    4. Your listener HTTP POSTs the complete, unaltered message back to PayPal.

    but I haven’t found any example where step 3 is followed. Why is that?

    • An HTTP 200 response is what’s standard when you do a POST or a GET (or anything I suppose). It’s the default, OK message. So the only time you actually need to do anything in regards to that (in general, not just here) is if you want to send something else. For example a 404 if something is not found or a 400 for a bad request.

  • Guest

    Hello, can you have the PDT and the IPN script on the same PHP page?

    • I suppose, but I’m not sure why you would want that. PDT is for people. IPN is for PayPal “robots”. Also, the IPN script should (have to?) return an empty HTTP 200 response. Something the PDT page won’t be doing, since it should be spitting out HTML for humans. I would definitely keep them separate, but I’m sure you could work it out somehow if you really, really have to.

  • Jo2013

    Hello, what version of PHP is required at a minimum to use Paypal’s PDT and IPN scripts?

    • Eh, not sure actually. Think I’ve used 5.2? At least I think that’s what they are running on currently on the sample page. Might work with 4.x. Just try it out, see if it crashes and if it does just try to substitute whatever it doesn’t like with something it does like?

  • levanlau

    Thank you for your post.It is very helpful with me, but I am new man who make the first paypal project,I face with an issue which cannot test in ipn simulator.When i enter my url on ‘IPN handler URL’ to test but ipn do not send me anything except for time out.I must do what now,please help.

    • I would start by making sure the URL is correct, try to post to it yourself and see if it works, etc. Either way, this is something you’ll have to figure out on your own as it’s impossible for me to say anything as I do not know your setup and I’m unable to do this kind of support. If it’s critical, maybe contact PayPal directly or post a question at

  • frank

    thanks for the above… it put me on the right path.. unfortunately (and I do mean unfortunately) my client is on a windows server.. can’t get past error message:error setting certificate verify locations: CAfile: cacert.pem, CApath: none, Tried adding:

    CURLOPT_CAPATH => FULL PATH.. Tried using the full path in CURLOPT_CAINFO => … got the same message for both.. any ideas??

    • I developed this on a Windows machine actually and that’s where the problem was to begin with. It worked for me. Don’t remember where I put the cacert.pem file, but all I really had to do was to set the two options you see in my code and the cacert.pem file in the same directory as the PHP file. Didn’t set CAPATH to anything at all.

          CURLOPT_CAINFO => 'cacert.pem',
  • Yacouba

    I just didn’t understand one thing.
    Why “require ‘ipn_handler.class.php’;” and where is this “ipn_handler.class.php” file from?

    • “I have put together a working sample you can check out over at Hopefully this tutorial and that sample can help you get started with all of this

      I also put together an IPN handler class you can use pretty much out of the box. The sample uses it, and it’s very simple. Just extend the class, override the process method and do what you need. Below is a simple example.”

  • nicolausai

    If i use the IPN simulator i can send perfectly valid messages to any cart that uses paypal so i can trick it? Just wondering because i need to secure carts

    • No, because the server sends the IPN message back to PayPal for verification and messages from the simulator would only validate towards the sandbox version of PayPal, not actual PayPal.

  • Ya Chieh Hsu

    Hello, I follow your tutorial but the payment_status in my received ipn message is always “pending” and the reason for that is “paymentreview”. Is that something to do with my payment button setting? I don’y quite understand that. I am under a sandbox environment. Thanls

  • c. willie

    fyi: reading your posts is a joy after spending hours over at PayPal’s house getting my ass kicked by their website

  • Jonathan

    Hi… Thanks for posting such an useful tutorial. I am trying this code and class but I keep receiving a “We could not send an IPN due to an HTTP error.” on Paypal IPN. I believe it has to do with the cacert.pem line. I downloaded the file you recommended and checked it is reachable. However IPN keeps showing that error. I made sure that $post_data holds the received data as well. Thanks again!

    • Try dumping stuff to a file. What your script gets sent to it, error messages from curl, etc. Other than that I can’t really help you unfortunately. This is PHP debugging, which you just need to figure out on your own, hehe. But yeah… that’s where I’d start anyways. Dump stuff that may make a difference to a file, figure out where it breaks.

      • Jonathan

        Im using Laravel and some of its functions… Ill ditch it for a while and test your plain PHP code. When I find out whats going on Ill post it here for any reference. Thanks anyway!

        • Jonathan

          Just in case someone finds it useful. I used your code with Laravel, which has a CSRF protection for post requests that was generating a conflict when Paypal sent the POST to my controller. I just had to add an exception to filters.php in order to prevent CSRF validation on the route where Paypal posts the notifications. Something like this:

          Route::filter(‘csrf’, function() {
          // Get named routes from configuration file
          $tmpStr = array(‘’,’paypal.notify’,’another.route’);
          //bypass on these routes
          $routename = Route::currentRouteName();
          if (!in_array($routename, $tmpStr)) {
          if (Session::getToken() != Input::get(‘csrf_token’) && Session::getToken() != Input::get(‘_token’))
          throw new IlluminateSessionTokenMismatchException;

  • Greateful

    Thank you, thank you.

  • Pingback: Paypal SandBox IPN always returns INVALID - Tech Magazine()

  • Pingback: Godaddy Email – Free Hosting 100 Mb Recurring – Cyber Sun()

  • Pingback: Godaddy Paypal Agreement – +ADw-/title+AD4-Hacked By TURKHACKTEAM.ORG Putin, knowingly and willfully planned airplane attack and the citizen on death. This has caused you to be you're a traitor. Now the citizens of nationalist feelings of the Russi()

  • Vivek

    When I simply used your code through my server, the handshake failed, could someone point me in the correct direction please?

  • James Atalo

    Great Tutorial!
    I encounter a problem using IPN Simulator. It will return “IPN was not sent, and the handshake was not verified. Please review your information.”
    But when I comment these 2 lines
    //$handler = new My_Ipn_Handler();
    then IPN Simulator returns “IPN was sent and the handshake was verified.”

    • Then your IPN script is probably failing for some reason and you need to find out why. Check error logs, try calling it yourself, etc. The message you get back from the simulator I think is just if the call returned an OK status code, so that’s why it thinks it’s all ok when you comment out “all” your code.

    • Jose Ortiz

      I also have the same issue. Were you able to resolve this?

    • milene santos

      I have the same problem. But, instead of status 200 (HTTP), I’m getting: 302 and consequently there is not a VERIFIED or INVALID response.

      I read it has something to do with ssl, but I am using https!! Does anyone know any solution?

      • 302 is a redirection response. Check that your URL is accurate?

  • Juan Manuel MosiΓ±o

    Hi! The listener at is not working now. I receive the following message: IPN was not sent, and the handshake was not verified. Please review your information. Any idea? My listener always responds with an INVALID response and I am trying to fix it with your tutorial. Your help will be haighly appreciated. Thank you in advance.

  • MartinS

    I’ve followed your IPN script but I keep getting “IPN was not sent, and the handshake was not verified. Please review your information” message from the simulator. I’ve downloaded the cacert.pem and placed it in the same directory as the listener.php and ipn_handler.class.php. Any way I can troubleshoot what is wrong? Also, can you explain what you mean by “Just extend the class, override the process method?” Thanks

    • Extending classes and overriding methods is a common object-oriented programming principle, so I won’t explain that to you, but instead tell you to go read up on it yourself: PHP Object Inheritance πŸ™‚

      As to how to troubleshoot, can’t really give you much help there either. Basically you just need to learn how to use your tools, and if your tools aren’t enough, you need to look for new ones. Anyways, some things I’d do: Look for warnings and error messages (and google them if you don’t understand them), try to calling your script manually, dump variables and things that could be of interest into a file to check if they are what they should be, etc. (and if you don’t understand what the variables should contain at the various steps, you need to look up the functions used and such and learn).

  • Terry Wysocki

    I’ve been getting the “IPN was not sent…” message for a while, and it looked like there was a problem with the simulator for a while, but it’s up now and I was able to post a couple of sample tests. I couldn’t get the script to run on my site, so I got a copy of the cacert.pem file as you suggest for SSL and it now works! Thanks a million!

  • Pingback: Paypal SandBox IPN always returns INVALID - QuestionFocus()