PHP

How to automatically process digital orders with PayPal

It’s not trivial to set up PayPal IPN (Instant Payment Notification). It requires a free Business (Premium) PayPal account, a web hosting account with PHP support, configured to allow creating HTTP socket connections using “fsockopen” function, a MySQL database set up on your web server, a mailing mechanism (for sending the product to the buyer) and a free Gmail account.

It may seem like a lot, but everything except the web hosting server (which you probably already have) is free, and once it is fully set up, you will be thankful for it as you’ll no longer have to manually process orders.

In this tutorial I’ll explain how to set up a skeleton PHP script that you can use for PayPal IPN requests. To make things easier, you can download the IPN script here.

Getting started

You will need:

  1. A PayPal account. Get one here.
  2. Any web hosting server with PHP script enabled, and ability to use function “fsockopen” (ask your web host how to enable it, look for Google tutorials on the subject (how to enable it in your PHP/Apache installation,) or move to a web server which enables it by default.)
  3. A Gmail account. If you don’t have one, register a Gmail account here.
  4. A copy of Swift Mailer PHP library, to “programmatically” connect to your Gmail, attach and mail files automatically.
  5. Knowledge of working with MySQL in PHP or a basic MySQL wrapper library.
  6. Create a new MySQL database on your web hosting server, create a user account (username, password) to access the database, then create a table on this database called “ipn”, and add in column names: ‘transaction_id’, ‘first_name’, ‘last_name’, ‘item_id’, ‘payment’, ‘time’.

How does the PayPal IPN work?

Every time there is any type of order (donation, one time payment, recurring payment, or other) credited to your PayPal account, PayPal processes your order silently, unless you allow it to additionally talk to a web page of your choice. Although it could be any script (ASP, or other) this tutorial assumes it is a PHP script. This script receives all data associated with the order.

Let’s create a new PHP file and call it ipn.php. This is the file PayPal will send a request to. Sending a request to a file simply means, that PayPal is sending you a copy of the information stored in an online order, associated with your PayPal account. PayPal sends such a message to ipn.php automatically, whenever your PayPal receives a payment or a refund transaction. But first, you need to set up the IPN URL, by logging in to your PayPal account.

In other words, you have to let PayPal know about the location of your PHP script on your web server. Follow the next three steps to add it to your PayPal account. If IPN is not enabled, the request will not be sent. This is a simple, yet crucial part:

  1. Log in to your PayPal account, hover over “Profile”, and select “My Selling Tools.
  2. Click “update” on the line where it says “Instant payment notifications.”
  3. Click orange “Edit Settings” button. Enter the web address of your PHP script, located on your server. Let’s say http://www.mywebsite.com/ipn.php, choose “Receive IPN messages (Enabled)” radio button, and click “Save.”

Using Swift Mailer to log into your Gmail account

Download the Swift Mailer library, unzip it and copy to your server. Simply place it on your server in a directory such as “swift/lib/swift_requored.php”, and you are all set.

Once we have swift mailer files copied to our server, we can now log in to Gmail accounts and automatically send out emails, and even include file attachments. In order to see how that is done, let’s take a look at the following example:

 // Your Gmail credentials
 $EMAIL = "my_account@gmail.com";
 $PASSWORD = "my_gmail_password";
 $FROM_SITE = "ipn@mysite.net";
 $FROM_NAME = "My Name or Website Name";

// Add swift mailer
 require_once('swift/lib/swift_required.php');
// Link to Gmail server
 $transport = Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, "ssl")->setUsername('$EMAIL')->setPassword('$PASSWORD');

// Create a new instance of Swift object
 $mailer0 = Swift_Mailer::newInstance($transport);

// $msg = Swift_Message::newInstance('Your Email Subject Line')
 ->setFrom(array('$FROM_SITE' => '$FROM_NAME'))
 ->setTo(array( "$EMAIL" ))
 ->setContentType('text/html')
 ->setBody("Email body message goes here");

// Attach PDF file -- file must exist at this location on your server
 $msg->attach(Swift_Attachment::fromPath('location/of/mybook-uj.pdf')->setFilename('mybook-uj.pdf'));

// Finally, send the message
 $mailer0->send($msg);

The part ::fromPath(‘location/of/mybook-uj.pdf’) indicates the physical location of the book on your server (invisible to the recipient) and ->setFilename(‘mybook-uj.pdf’) part indicates the filename of the file, as it will appear to the recipient in their inbox.

$FROM_SITE name is the address of the server, it can be anything but it’s best if it matches a real email address from your web server. Make sure the part after @ is the web address where the ipn.php script is hosted, to avoid triggering some spam filter alerts. $FROM_NAME is the name the recipient of the email will see in their inbox, it can be anything.

Using the MySQL database

You can use plain MySQL, but to make the already difficult task easier, I have written a MySQL library in PHP. Simply download it here, and include the files at the top of ipn.php script, as will be shown shortly.

You will have to have a database set up on your web host. Follow your web host’s instructions on how to do that. Once you set it up, open file class.MysqlDatabase.php and replace it with your own database values:

class MysqlConfig2 {
    public static $HOST = "localhost"; // Web server, localhost or an IP address of your web server
    public static $USER = "root"; // Username, root or whatever username you created
    public static $PASSWORD = "123456"; // Replace with your password
    public static $CATALOG = "master"; // Replace with your database name
}

Replace localhost with your public database IP address, like 98.23.77.12 or if you cannot make your database public, you should be able to use “localhost” with no problem.

Save the file class.MysqlDatabase.php with changes from above and upload it to your web server, together with function.Database.php from the archive. Then include both of the files at the beginning of ipn.php as will be shown shortly.

The MySQL database wrapper library works in the following way. To check if the database connection is ready, meaning the above call has succeeded:

if ($Connection->isReady())
{
    // Connected...
}

Now that we are sure the connection was successful, get an item entry from the database, and check if it exists. If it doesn’t exist, insert it. If it exists, do nothing:

if ($Connection->isReady()) {
    // Get "time" of a transaction
    // from table called "ipn"
    // whose "transaction_id" entry is set to 123
    $item = db::get("`ipn`", "`time`", "`transaction_id` = '123'");

    // Check if the above item exists
    if (empty($item))
    {
        // It doesn't exist, it must be a new transaction
    }
    else
    {
        // It already exists, do nothing.
    }
}

You’ll see the method db::get being used in ipn.php to access the database, for checking if an item already exists, or to insert a new item into the database if it doesn’t.

Let’s say the item doesn’t exist, then we will use the db::insert method to insert an entry into the database:

// Insert this transaction
$names = array('transaction_id', 'first_name', 'last_name', 'item_id', 'payment', 'time');
$values = array($transaction_id, $first_name, $last_name, $item_id, $payment, time())
db::insert("`ipn`", $names, $values);

Note that names are the database column names. Add other columns, if you wish to store buyer’s email address, or other information. But the transaction_id and item_id are the most crucial “must-have” columns. Make sure to have them!

Writing ipn.php script in PHP

There is no PHP library provided by PayPal, all the code needs to be written by ourselves. PayPal just sends an HTTP request to ipn.php, and what we do with the data we receive is entirely up to us.

In this tutorial, we’ll have our script log in to your Gmail account using Swift Mailer library, attach your digital file to the email, write a brief message to the buyer, and send the digital product to the customer.

<?php

/* The following values must be your Gmail email address and password */
$EMAIL = "replace_with_my_email_address@gmail.com";
$PASSWORD = "replace_with_my_gmail_password";
$FROM = "Greg Sidelnikov"; // Your email "from" name

/* To properly implement PayPal IPN, you need either knowledge of MySQL and write your own queries using PHP function mysql_query, or download any basic MySQL PHP library. We have to have some basic MySQL database access, So that we can verify each IPN request from PayPal, this way we don't process the same order more than once. */

include("php/function.Database.php"); // Database helper functions
include("php/class.Database.php"); // The core library

/* The following is the swift mailer library, we must include it to send automatic emails from Gmail, to the buyer. */

require_once('swift/lib/swift_required.php');

/* PayPal will send a POST request to this file. Let's process it.
Here we are just making sure important items are received: */

if (!isset($_POST['payer_email'])) $_POST['payer_email'] = "";
if (!isset($_POST['first_name'])) $_POST['first_name'] = "";
if (!isset($_POST['last_name'])) $_POST['last_name'] = "";
if (!isset($_POST['payment_status'])) $_POST['payment_status'] = "";

// Read the post from PayPal system and add 'cmd' to send back
$req = 'cmd=_notify-validate';
$req2 = 'cmd=_notify-validate';
$res = '<table><tr><td>cmd</td><td>_notify-validate</td></tr>';

foreach ($_POST as $key => $value) {
$value = urlencode( stripslashes( $value ) );
$req .= "&$key=$value";
$res .= "<tr><td>" . $key . "</td><td>" . urldecode($value) . "</td></tr>"; // Clean up for email notification
}

$res .= "</table>";

// Now that we stored the exact copy of the POST data that was sent to us, let's rewrite it so it looks acceptable for displaying

// Reformat POST values to look normal (Removes %20, %40, etc.)
foreach ($_POST as $key => $value)
$_POST[$key] = urldecode( $value );

// Let's assign POST values to variables:
$transaction_id = $_POST['txn_id'];
$payment = $_POST['mc_gross']; // 'payment_gross'
$first_name = $_POST['first_name'];
$last_name = $_POST['last_name'];
$item_id = $_POST['item_number'];
$payer_email = $_POST['payer_email'];
$payment_status = $_POST['payment_status'];
$txn_type = $_POST['txn_type'];
$item_number = $_POST['item_number'];
$item_number1 = $_POST['item_number1'];

// For multi-level drop down menu buttons, for some reason IPN sends
// item number as "item_number1" instead of "item_number", so we
// have to rename it. Note, that in a multi-level drop down menu,
// $_POST["item_number"] doesn't even exist, and to make things less
// confusing later in the code, we give it the value from
// $_POST["item_number1"]. This is the ID name of the PayPal button
// you created.

if (isset($item_number1)) { $item_number = $item_id = $item_number1; }

We’re done with basic preparation, now let’s actually open a socket connection to PayPal’s IPN server:

/* Receive the data from PayPal
Note, if testing in PayPal Sandbox replace the uncommented line below with the one below:
$fp = fsockopen ('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30); */

// Open socket connection
$fp = fsockopen ('ssl://www.paypal.com', 443, $errno, $errstr, 30);

// Check the socket connection availability, should return true:
if (!$fp)
{
echo "fsocketopen failed";
// HTTP ERROR : Send yourself an email to let you know that this script failed to open the socket.

// This should never happen if everything is Ok, however it's nice to be notified by email when it does happen
$transport = Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, "ssl")->setUsername('$EMAIL')->setPassword('$PASSWORD');
$mailer0 = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('IPN HTTP Error')
->setFrom(array('ipn@mobilejquery.net' => 'IPN Sale'))
->setTo(array( "$EMAIL" ))
->setContentType('text/html')
->setBody("Your PayPal IPN script encountered an HTTP error. \$fp = fsockopen(...) function failed with following errors:<br>Error Code: $errno<br>Error Type: $errstr");

$mailer0->send($msg);

exit;

} else {

If we got here, it means the connection was successful. Note, that if the request is not successful, the code above will send an email to myself, just to let me know this script was unable to process the order. This should never happen in a healthy IPN system, and rarely does once the script is properly set up. But while testing, and making sure it actually works (anything can go wrong) it’s best to have that test in place.

Now that we got connected to the IPN server, we for sure know that we will be sending emails. So, create the main swift mailer object for later use.

// Create mailer object, we will use it throughout the rest of the code to send emails.
$transport = Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, "ssl") >setUsername('$EMAIL')->setPassword('$PASSWORD');

We have established the connection to PayPal’s IPN server and set up a swift mailer object. Next, we have to send a message back to the IPN server, in order to confirm we received it. It’s like playing ping pong with PayPal.

Here we will create a basic HTTP header ourselves, and use it to send the same data, that we received from PayPal, back to PayPal. This is how their server knows that we received it.

$header = "POST /cgi-bin/webscr HTTP/1.1\r\n";
//if you are using sandbox, uncomment the following line, and comment the one right below it
//$header .= "Host: www.sandbox.paypal.com\r\n"; // MUST be second line, or it will fail with Bad Request.

$header .= "Host: www.paypal.com\r\n"; // MUST be second line, or it will fail with Bad Request.

$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($req) . "\r\n";
$header .= "Connection: close\r\n\r\n";

/* Return the response back to PayPal -- $fp is the opened socket connection -- */
fputs ($fp, $header . $req); // -- send it back

So at this point we have streamed the data back to PayPal. It will match it with their stuff and return a response back to us, saying either this data is INVALID, or it will say VERIFIED.

// Now PayPal will return an answer on the same socket -- and we must read it and execute an action based on what was returned.

// Read the answer from PayPal -- It will be just one word, either "VERIFIED" or "INVALID"

// If verified was returned -- it's ok to execute the action, e.g.: send the item to the buyer
$read = "";
$full = "";
$i = 0;
$http = "";
while (!feof($fp))
{
$read = fgets($fp, 1024); // Read one line at a time until we find VERIFIED or INVALID (Usually line 14)
$http .= $read . "<br/>"; // $http is used for tracking at the bottom of transaction emails (just to see what the PayPal response was...)
if (preg_match('/VERIFIED/', $read, $matches) == 1)
{

Excellent! Our HTTP transaction was verified as being legitimate. Now we have to check if the order itself was completed. Notice there is a difference between a “VERIFIED” HTTP transaction, and a “Completed” order. A Completed order means money appeared in your PayPal account. But a “VERIFIED” HTTP transaction will be true even on things like refunds, money returns, credit card charge backs, and everything else that may not necessarily be an actual order, let alone completed.

An e-check that is still processing will not show up as a “Completed” order until it clears. You don’t have to add any special lines of code to process e-checks that have not been cleared. Whenever it clears, you will receive a second request from PayPal that will register here anyway.

But, if we receive an order that was “Completed”, rest assured, the payment went through. This is why it’s important to check for that here, otherwise you may be sending your digital product to someone whose payment did not go through yet.

 if (preg_match('/Completed/', $payment_status, $matches) == 1) // Payment was actually completed, not "Pending", "Cancelled" or "Refunded", etc
 {
 

Let’s connect to the database and store the transaction ID. This is required. The answer for this requirement is simple. PayPal’s IPN server sends multiple requests to ipn.php, even after it has already sent one. It does that until we return a response to it. Then it quiets down. But what if something goes wrong? So your script may get stuck processing multiple requests, and you may send multiple copies of your digital product to the same buyer. This happened to me once, and the script sent 50 copies of the book to a buyer, flooding his email inbox!

You can use any other MySQL library to access your database here:

$Connection = new db(); // Create the MySQL connection object, connect to the database and guarantee a successful connection
if ($Connection->isReady())
{
// Check if the item with this transaction already exists
$item = db::get("`ipn`", "`time`", "`transaction_id` = '" . $transaction_id . "'");
// It doesn't exist,
if (empty($item))
{
// Insert this transaction
db::insert("`ipn`", array('transaction_id', 'first_name', 'last_name', 'item_id', 'payment', 'time'), array($transaction_id, $first_name, $last_name, $item_id, $payment, time()));
$ReferenceURL = $_REQUEST['ref'];

It’s important to draw a distinction between products, this code will still happen for donations, money sends, etc. (everything.) So we check $_POST[“item_number”] to make sure it matches the item transaction “number”

Below we’ll match the transaction with the product item code (this is the ID you give your digital product when you create the PayPal button). My products’ IDs are

  • fund-js-course
  • digital-download
  • jqgems-64
  • ujq-plus-gems
  • jqmobile-25

Yours will be different.

// ** Multi-level Donation
if (preg_match('/fund-js-course/', $item_number1, $matches) == 1)
{
}

// ** Understanding jQuery Book
if (preg_match('/digital-download/', $item_number, $matches) == 1)
{
// Email to myself
$mailerg = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('+1 IPN Sale {uj} via ' . $ReferenceURL) >setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL")) >setContentType('text/html')->setBody($res);
$mailerg->send($msg);
// Send the book to the payer...
$mailer2 = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('Thank You for Buying Understanding jQuery')
->setFrom(array('$EMAIL' => '$NAME'))
->setTo(array( $payer_email ))
->setBcc(array('$EMAIL'))
->setContentType('text/html')
->setBody('<p>Thank you for buying my book. {<a href = "#">download pdf</a>} I also attached the PDF to this message.</p><p><i>Ps</i>: Things happen; please let me know if you have trouble downloading it.</p><p>Greg Sidelnikov</p>');
// Attach PDF -- file must exist at this location on your server
$msg->attach(Swift_Attachment::fromPath('location/of/mybook-uj.pdf')->setFilename('mybook-uj.pdf'));
$mailer2->send($msg);
}

// ** jQuery GEMS Book
if (preg_match('/jqgems-64/', $item_number, $matches) == 1)
{
// Email to myself
$mailerg = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('+1 IPN Sale {gems} via ' . $ReferenceURL)->setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL"))->setContentType('text/html')->setBody($res);
$mailerg->send($msg);
// Send the book to the payer...
$mailer2 = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('Thank You for Buying jQuery GEMS')
->setFrom(array('$EMAIL' => $NAME))
->setTo(array( $payer_email ))
->setBcc(array('$EMAIL'))
->setContentType('text/html')
->setBody('<p>Thank you for buying my book. {<a href = "#">download pdf</a>} I also attached the PDF to this message.</p><p><i>Ps</i>: Things happen; please let me know if you have trouble downloading it.</p><p>Greg Sidelnikov</p>');
// Attach PDF --
$msg->attach(Swift_Attachment::fromPath('location/of/mybook2-gems.pdf')->setFilename('mybook2-gems.pdf'));
$mailer2->send($msg);
}

// ** 2 Book Bundle
if (preg_match('/ujq-plus-gems/', $item_number, $matches) == 1)
{
// Email to myself
$mailerg = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('+1 IPN Sale {bundle} via ' . $ReferenceURL)->setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL"))->setContentType('text/html')->setBody($res);
$mailerg->send($msg);
// Send the book to the payer...
 $mailer2 = Swift_Mailer::newInstance($transport);
 $msg = Swift_Message::newInstance('Thank You for Buying Understanding jQuery & jQuery GEMS')
 ->setFrom(array('$EMAIL' => $NAME))
 ->setTo(array( $payer_email ))
 ->setBcc(array('$EMAIL'))
 ->setContentType('text/html')
 ->setBody('<p>Thank you for buying my books. {<a href = "#">download 1</a>} {<a href = "http://www.mobilejquery.net/book/jquery-gems-v1-64.pdf">download 2</a>} I also attached the PDF files to this message.</p><p><i>Ps</i>: Things happen; please let me know if you have trouble downloading them.</p><p>Greg Sidelnikov</p>');
// Attach PDF --
 $msg->attach(Swift_Attachment::fromPath('location/of/mybook-uj.pdf')->setFilename('mybook-uj.pdf'));
 $msg->attach(Swift_Attachment::fromPath('location/of/mybook2-gems.pdf')->setFilename('mybook2-gems.pdf'));
 $mailer2->send($msg);
 }

// ** jQuery Mobile and Beyond...
 if (preg_match('/jqmobile-25/', $item_number, $matches) == 1)
 {
 // Email to myself
 $mailerg = Swift_Mailer::newInstance($transport);
 $msg = Swift_Message::newInstance('+1 IPN Sale {mobile} via ' . $ReferenceURL)->setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL"))->setContentType('text/html')->setBody($res);
 $mailerg->send($msg);
// Send the book to the payer...
$mailer2 = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('You have Pre-ordered jQuery Mobile and Beyond')
->setFrom(array('$EMAIL' => $NAME))
->setTo(array( $payer_email ))
->setBcc(array('$EMAIL'))
->setContentType('text/html')
->setBody('<p>Thank you for pre-ordering my new book "jQuery Mobile and Beyond..." {<a href = "mobile">book homepage</a>} In this volume I cover everything you need to know to get started with making mobile websites and applications.</p><p>I am still working on it! And you will receive it on 03/15/2013 which is really soon now. As a result of this transaction you got a 30% off discount (which is about $7.50) and I get more time to edit the book to bring it to the optimal condition so you can enjoy it more.</p><p><i>Ps</i>: Questions happen; please let me know if you have any questions regarding your purchase.</p><p>Greg Sidelnikov</p>');
// DO NOT Attach PDF -- It's still only a pre-order...
$mailer2->send($msg);
}
}
else
{
/* This transaction already exists */
}
}
else
{
}
}
}

Finally, do something when the data does not match.

else if (preg_match('/INVALID/', $read, $matches) == 1) { // Response is INVALID

// Anote to myself
$transport = Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, "ssl")->setUsername('$EMAIL')->setPassword('$PASSWORD');
$mailerg = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('IPN=Invalid - Payment Status - ' . $_POST['payment_status'])->setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL"))->setContentType('text/html')->setBody($res);
$mailerg->send($msg);
}
}

Below I added code to send myself an email, just to let my know that this request was finalized. It also has a copy of all data received from PayPal in their POST request. I mail it to myself, sometimes it’s helpful for verifying things with customers.

// This call is here to ensure there was no id error
// To myself
$mailerg = Swift_Mailer::newInstance($transport);
$msg = Swift_Message::newInstance('IPN Transaction Finalizer <' . $payer_email . '>')->setFrom(array('$EMAIL' => $NAME))->setTo(array("$EMAIL"))->setContentType('text/html')->setBody($res . "<hr>" . $http);
$mailerg->send($msg);
}

?>

You can download all of this code here.

Remove or change preg_replace(‘product-id-1’, …) lines with IDs of buttons and products you have set up and you should be ready to process online orders automatically. I use “if” statements to separate each product. If you only have one product, just remove all “if” statements except one. You still need to check for the product id, even if you have one product.

Greg Sidelnikov has been writing programming tutorials since 2004, he tweets free tutorials/demos at @gregsidelnkiov. More articles by Greg Sidelnikov
Home CSS Deals DesignBombs HTML HTML5 JavaScript jQuery Miscellaneous Mobile MySQL News PHP Resources Security Snippet Tools Tutorial Web Development Web Services WordPress