PHP Tutorial: PayPal Payment Data Transfers (PDT)

MoneySay you have a PayPal “Buy Now”-button on your website and you have assigned return URLs like and You can then welcome the user back after a successful payment. But what if you wanted to say something more interesting than just “hey, welcome back” when they click on that “Return to Merchant”-button? And can you know if the order was actually done or canceled? Maybe you’d like to log the transaction in your database and mark a payment as complete or something like that too? In that case you sure can’t trust a simple flag in the address bar…

Payment Data Transfer (PDT) is a secure method to retrieve the details about a PayPal transaction so that you can display them to your customer. It is used in combination with Website Payments Standard, so that after a customer returns to your website after paying on the PayPal site, they can instantly view a confirmation message with the details of the transaction. — PayPal

I’ve tried to figure out how to use PDT and found that most samples and classes to build from are usually quite ugly, old or outdated. I didn’t find them too useful anyways… So, therefore, I’ve tried to do my own thing based on the documentation found on the PayPal Developer websites. (Seriously, how many versions of documents and developer websites do they have anyways? It’s like a complete jungle…)

Since the documentation was a bit of a mess, I thought I make a small tutorial on the steps needed to get started. That way I can learn it better myself and hopefully help some other poor souls that need to figure this stuff out as well. Please provide feedback if you have any! Would love to make this page nicer and clearer if possible 🙂

Step 0: Sign up for a Developer Account

Before you start playing around with PayPal stuff, I would recommend that you get a PayPal developer account. You should also set up a business account in the PayPal Sandbox which you can then use instead of your real account. A lot safer, cheaper and easier than using a real one…

Step 1: Enable PDT for your PayPal account

For this step I’ll just quote their PDT page (one of many, it seems):

Apparently they have changed their UI now, so the instructions in their PDT documentation is wrong… Thanks for the heads up Yuriy! The description below should be correct now 🙂

To use PDT, you must activate PDT and Auto Return in your PayPal account profile. You must also acquire a PDT identity token, which is used in all PDT communication you send to PayPal.

Follow these steps to configure your account for PDT:

  1. Log in to your PayPal account.
  2. Click the Profile subtab.
  3. Click the My Selling Tools button in the left column.
  4. Find the Selling online section and click Update in the Website preferences row.
  5. Under Auto Return for Website Payments, click the On radio button.
  6. For the Return URL, enter the URL on your site that will receive the transaction ID posted by PayPal after a customer payment.
  7. Under Payment Data Transfer, click the On radio button.
  8. Click Save.
  9. Find the Selling online section and click Update in the Website preferences row again.
  10. Scroll down to the Payment Data Transfer section of the page and take note of your PDT identity token.

The last point is particularly important and you should not only view the token, but note it down. Reason being that you need this in your code when we’re going to talk to PayPal later.

Screenshot of PayPal Payment PreferencesAs an aside, I would also recommend you turn off the “Block Non-encrypted Website Payment”, at least while testing, since it’s a lot easier to make PayPal buttons on the fly without encrypting them. I currently don’t know how, but I’ll probably try to figure out soon. I would also recommend that you set PayPal Account Optional, so that people can make payments without signing up (since some people don’t want to be bothered with that). To the right I have put a screen shot of that page with the mentioned options marked in yellow. Click on it for full-size view 🙂

Step 2: Create a button

The PayPal buttons consist of a form which has a number of hidden key/value pairs and a submit button. You can find info on how to make those in their Website Payments Standard Integration Guide so I won’t explain how to do that here. What I would like to mention in this case though, is that you can, if you need to, override the return URL that you set in the previous step for specific buttons. For example you can return them to the order they paid for rather than just a general return page.

<input type="hidden" name="return" value="" />
<input type="hidden" name="cancel_return" value="" />

I use the same URL for both since we will check things with PDT anyways. Here is an example button with the data I use in this tutorial:

<form action="" method="post" accept-charset="utf-8">
    <input type="hidden" name="cmd" value="_xclick" />
    <input type="hidden" name="charset" value="utf-8" />
    <input type="hidden" name="business" value="" />
    <input type="hidden" name="item_name" value="Stuffed bear" />
    <input type="hidden" name="item_number" value="BEAR05" />
    <input type="hidden" name="amount" value="20.00" />
    <input type="hidden" name="currency_code" value="USD" />
    <input type="hidden" name="return" value="" />
    <input type="hidden" name="cancel_return" value="" />
    <input type="hidden" name="bn" value="Business_BuyNow_WPS_SE" />
    <input type="image" src="" name="submit" alt="Buy Now" />

So, the user will click the button, go through the payment procedure, and at the end there will be a link/button to return to the merchant. When that is clicked, we get to the next step.

Step 3: Catch the return

The URL they are returned to could for example look like this (split for easier reading):

As you can see we get some GET parameters to work with here, but the only one you need is the Transaction ID, tx:

  $tx = $_GET['tx'];
  // Further processing

The reason we don’t care about the rest is that they’re not trustworthy at all. Since they are GET parameters we can easily tamper with them in the address bar of our browser, and we just can’t have any of that, can we?

To get good and trustworthy data we need to send this transaction id back to PayPal together with our PDT Identity Token. If the id and token are valid, we will get real and actual transaction details back in return.

Step 4: Do a POST request back to PayPal

Sending the Transaction ID and our PDT Identity Token back to PayPal can be done in various ways from PHP. I used cURL which I found to be pretty clean and easy to use.

// Init cURL
$request = curl_init();

// Set request options
curl_setopt_array($request, array
  CURLOPT_URL => '',
  CURLOPT_POSTFIELDS => http_build_query(array
      'cmd' => '_notify-synch',
      'tx' => $tx,
      'at' => $your_pdt_identity_token,
  // 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

The cURL options we set means roughly: Use this URL, do a POST, send these variables, return the transfer as a string when I call curl_exec and do not include the HTTP headers in that string. The reason behind the last two are in this post. I’ve commented them out, and if it works without them, just leave them out. For me it worked fine on one server, while on another I had to add them.

Note: For security reasons, PayPal will only allow you to use the transaction id to get a response up to 5 times. After that it will be invalidated and you will get an error back instead of the transaction data. Fair enough for regular use, but found it a bit annoying while developing since it took some tries to get the result I wanted 😛

Step 5: Check the response

The response will be a string that looks something like this:


To make sure everything is well we must check that the status code is 200 and that the first line says SUCCESS. On failure it would say FAIL, or something like that. We can do that like this:

if($status == 200 AND strpos($response, 'SUCCESS') === 0)
    // Further processing
    // Log the error, ignore it, whatever

That response is not very useful right now though. Even if it is valid, the string is just, well, a long string. Not to mention it’s URL encoded and possibly in the wrong character encoding. We want to clean it up and turn it into a much more handy associative array.

Step 6: Clean up the response

We want to remove the first line, URL decode the string, turn it into an associative array and convert the encoding if needed. This is how you might do that:

// Remove SUCCESS part (7 characters long)
$response = substr($response, 7);

// URL decode
$response = urldecode($response);

// Turn into associative array
preg_match_all('/^([^=\s]++)=(.*+)/m', $response, $m, PREG_PATTERN_ORDER);
$response = array_combine($m[1], $m[2]);

// Fix character encoding if different from UTF-8 (in my case)
if(isset($response['charset']) AND strtoupper($response['charset']) !== 'UTF-8')
  foreach($response as $key => &$value)
    $value = mb_convert_encoding($value, 'UTF-8', $response['charset']);
  $response['charset_original'] = $response['charset'];
  $response['charset'] = 'UTF-8';

// Sort on keys for readability (handy when debugging)

The $response should now contain a nice and sorted associative array with the correct encoding and it’s ready to be used for whatever purpose you need.

Step 7: What now?

What you do with the validated and cleaned up data depends on what you need and what you want. You could use it to display a nice helpful message to the user, store it somewhere, maybe mark something as paid in your database, et cetera. You should also most likely check that the data you have gotten corresponds to what you expected. For example if the amount they paid matches the amount they should have paid and so on.

Important thing to remember about PDT

You are actually not guaranteed to get this PDT data. Unless the user clicks that last button in the payment process at PayPal and returns to your page, you will get nothing. They could for example close the browser tab when the payment is done or the power could go out just before they had the opportunity to. Theoretically it could happen!

If you want something that is fully reliable, you need to have a look at PayPal Instant Payment Notifications (IPN). PayPal sends these notifications to a certain URL you have provided behind the scenes and it is not part what the user does or does not do. They may not be instantaneous, but you are guaranteed to eventually get them.

What you most likely want is a combination of these two. Use PDT to give the returning users a quick response and store what you can/need in the database. Then use IPN to fill out the gaps of missed PDTs or missing transaction details (IPN will give you many more details than PDT does).

It took some time, but I have finally finished a tutorial on IPN Check it out if you’re interested 8)

Working sample

Since the code above was a bit broken up, I have gathered it all in a single example page that should be operational. It contains a simple Buy Now button and it will display the PDT response when you return from the fake purchase. Since it uses the PayPal Sandbox you need to be logged in to your developer account. For the payment you can use one of your existing test accounts or you can pretend to be a person that just wants to use his credit card without an account (If you need help coming up with a fake credit card number that validates, have a look at

You find it over at

The process_pdt function you can see in the source code should be pretty self-contained and you should be able to just drop it in to whatever you are working on and use it out of the box. Just remember to fill in your actual identity token 😉

And that’s the end of this tutorial!

Think this was my first longish in-depth post, so if you have any feedback, found it helpful, please leave a comment. Also do that if something is unclear, could be improved or is flat out wrong. Especially if it’s flat out wrong. There’s a lot of wrong stuff on the Internet, and I don’t want to contribute to that 😛