Instant Payment Notification


Processing Instant Payment Notification is relatively easy, but it must be configured on PayPal's site before it can be used. Under the My Account tab, and Profile subtab, select Instant Payment Notification Preferences, which is under the Selling Preferences heading. Here you must enable IPN, and give it a URL to post to when a payment has been made.

Note 

Both portions of the IPN process (PayPal notifying your server and your server confirming the transaction with PayPal) can be performed over either HTTP or HTTPS. If the option is available to you, I would recommend taking advantage of this added layer of security. Although you will not be receiving payment information (such as credit card or bank account numbers), you will receive real names and addresses. Receiving the notification over HTTPS requires only that your server have a valid SSL certificate, and that the script be placed in a suitable location. Confirming the transaction with PayPal, however, requires that PHP was compiled with SSL support. To check your version of PHP, look at the output of the phpinfo() command, specifically under Registered PHP Streams and Registered Stream Socket Transports. In order for PHP to initiate communications over HTTPS, you need HTTP in the former and SSL and TLS in the latter. Detailed information on the configuration of the development box is available in Appendix C.

After IPN has been enabled, PayPal will start sending a POST request each and every time a transaction is made against your account as soon as the transaction is processed (this isn't a batch processing system). For example, the POST request generated after clicking the payment button shown earlier would look something like this:

Note 

Note that you will receive a post whenever a transaction is made against your account; this includes payments, refunds, charge backs, and so on. Be sure to code your applications to handle these different notifications.

 [mc_gross] => 29.95 [address_status] => confirmed [payer_id] => 7WPEM4M7HBDNW [tax] => 0.00 [address_street] => 123 Sesame Street [payment_date] => 10:30:15 Apr 07, 2005 PDT [payment_status] => Completed [address_zip] => N2K 1K9 [first_name] => Big [mc_fee] => 1.47 [address_name] => Big Bird [notify_version] => 1.6 [custom] => [payer_status] => verified [business] => stb@preinheimer.com [address_country] => Canada [address_city] => CaringVille [quantity] => 1 [payer_email] => bigbird@example.com [verify_sign] => AXZRCbqawxfwAUFhW2J.g21PQqAiA0ONoj4WX-jxbANvyGgwXHThirqT [txn_id] => 5NP73724H5715082L [payment_type] => instant [last_name] => Bird [address_state] => Ontario [receiver_email] => stb@preinheimer.com [payment_fee] => 1.47 [receiver_id] => L3D7XTLLRU82J [txn_type] => web_accept [item_name] => Professional Web APIs with PHP [mc_currency] => USD [item_number] => 0764589547 [test_ipn] => 1 [payment_gross] => 29.95 

The majority of the information presented in the post is self-explanatory. It contains the information from the purchaser (name, address, email, and so on), information about the selected product (name, item number, cost), and information about the transaction itself (fees, transaction IDs, and quantity). There are a couple things to note with this information: First, the txn_id (transaction id) value presented here is for PayPal's payment to you — it will not match any transaction id the purchaser was given. Second, the mc_fee and payment_fee values will only match each other when the transaction is completed in USD. The amount deposited into your account is equivalent to the mc_gross value minus the mc_fee value. Third, the payment_status value is critical. In this situation, the value is Completed, however it may also be set to Pending, in which case there will also be a pending_reason field, which will include more information as to why the payment is still pending.

Do not ship the product while payment is still pending because the payment itself can still fail for a variety of reasons. Once the payment is cleared, another IPN post will be made by PayPal.

Note 

When a transaction is received and payment is still pending, it would be a great idea to email the customer and let them know what is going on. The general expectation with online purchases is instant notification or (in the case of online products or services) instant receipt. Not informing the user that the payment has been noted, but not yet received, will likely result in a higher customer service work-load, and canceled transactions or refund requests.

Processing the IPN

Processing an IPN has three essential steps: validating the post with PayPal, checking the purchase information against your own database (for product existence and price/product mismatch), and passing off the information to your ordering system. If you fail to validate the IPN with PayPal and confirm pricing information with your own local database, you open yourself up to abuse in two ways. First, malicious users can fake an IPN request to your server of an appropriate amount for a particular item, which your system will then treat as any other order and process accordingly. Second, users could send a token amount of money ($0.05) and order an expensive item. I've seen production systems vulnerable to either or both attacks.

Validating the IPN with PayPal

Validating the IPN is actually very easy, and quite well documented. You simply send the $_POST variable back to PayPal, adding one element of your own to indicate that this is a validation attempt. This is to ensure that the notification did in fact originate with PayPal, rather than an attacker attempting to mimic the notification.

The function has a few basic steps. First, it processes the received $_POST to generate a usable POST request of its own. Second, it creates the socket (returning an error if necessary). Third, it processes PayPal's response. Finally, based on that response, it returns TRUE for a valid IPN and FALSE for an invalid IPN.

 function verifyIPN($data) {   $postdata = "";   $response = array();   foreach($data as $var=>$val)   {     $postdata .= $var . "=" . urlencode($val) . "&";   }   $postdata.="cmd=_notify-validate"; 

The required variables are declared and the received data is processed to be used for its own POST request. Though it does look tempting, implode() won't work with this associative array. An additional value, cmd=_notify-validate, is added to indicate that this is a validation attempt.

   $fp=@fsockopen("ssl://www.sandbox.paypal.com" ,"443",$errnum,$errstr,30); 

Here the socket itself is opened. As indicated earlier, connections are being made over the Secure Sockets Layer (which uses port 443) to avoid any interception of customer data. Any error information will be saved in the indicated variables, and the script will wait 30 seconds before timing out.

   if(!$fp)   {     return "$errnum: $errstr";   } else   {     fputs($fp, "POST /cgi-bin/webscr HTTP/1.1\r\n");     fputs($fp, "Host: www.sandbox.paypal.com\r\n");     fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");     fputs($fp, "Content-length: ".strlen($postdata)."\r\n");     fputs($fp, "Connection: close\r\n\r\n");     fputs($fp, $postdata . "\r\n\r\n");     while(!feof($fp)) { $response[]=@fgets($fp, 1024); }     fclose($fp);   } 

First, the socket is checked to confirm a successful connection, and barring any problems, the POST request is sent. Notice the use of the strlen() function when sending Content-Length. This is the easiest way to get the right value. I prefer to generate that value during the connection, because generating it earlier has led to problems with code being introduced after the fact that changes the data (and hence the length).

   $response = implode("\n", $response);   if(eregi("VERIFIED",$response))   {     return true;   }else   {     return false;   } } 

The response is initially saved in an array; here it is imploded to exist within a flat string. The eregi() function is used to check for the specified text within the following text, and based on those results TRUE or FALSE is returned.

Confirming Product Information

The exact process required to confirm product information depends extensively on your needs, the way you store product information, and so on. That being said, here are a few tips:

  • Remember the goal of this step: ensure that the amount received is correct for the product (and quantity) ordered.

  • Double-check any shipping or tax amounts.

  • Double-check currency if you accept payments in multiple currencies.

  • If your site offers specials or discounts, remember to check those as well, including confirming the customer's eligibility for the unique price.

  • Stress to the entire team that any price changes must be made in the appropriate places (like the database) rather than directly on the website in order for the system to work.

If the system is unable to match an incoming order with a product in the database, several actions should be taken. First, the customer should be notified that there was a problem with the order, that the problem isn't their fault, and that you will be looking into it shortly. Many customers will panic if they don't receive an email shortly after placing an order, so by contacting them and taking ownership of any problems, you can do a lot to allay their concerns. Second, the appropriate team should be notified of the issue. In this case it should probably be the customer service group, which should be able to determine if this is in fact a valid order (and process it accordingly), and if not, forward it on to the security team.

Here is a simple example of confirming an order against a database:

 function confirmProduct($id, $name, $amount) {   if (!(ctype_digit($id) && is_numeric($amount)))   {     return false;   }else   {     $name = mysql_escape_string($name);   } 

Confirming the order in this case only requires three variables: the ID of the product, the product's name, and the amount sent. The product ID should always be just digits, and the amount sent should be a numerical value. These checks can be confirmed before looking for the product in the database. This also ensures that both elements are safe to be put into a SQL query. If both values are appropriate, the product name is prepared for the SQL query.

   $query = "SELECT id FROM products WHERE `id` = ‘$id' AND `p_name` = ‘$name' AND     `cost` = ‘$amount' LIMIT 1";   if (rowCount($query) == 1)   {     return true;   }else   {     return false;   } } 

A simple query is done to check for the existence of the exact product within the database; the return value is based on that check.

As you can see, the process of checking every order against a local database of valid orders can be pretty simple, but is unfortunately often overlooked, leaving systems open to any number of fraudulent orders.

Note 

As mentioned earlier, I've personally seen payment systems (not always PayPal) vulnerable to either or both methods of attack mentioned here. It's critical that you validate every aspect of the incoming request to determine its validity.

Passing Off to the Regular Order System

How this section is handled is entirely dependent on the rest of your infrastructure. There are two things to consider here: First, remember that this script will be called with every order, and will hold onto the resources of your web server (and the TCP connection used for the request) until it exits. If the scripts used to process an order take anything more than a few moments to complete, you may end up leaving the web server crawling if many orders are placed at once. Second, the example script shown here has not yet done anything to the POST data received; it will need to be prepared appropriately before being used in any new database queries.

Putting It All Together

Combining all the code thus far, as well as a few minor additions, you have your Instant Payment Notification script, ipn.php:

 <?php   //Step 0. Record the transaction     ob_start();     echo date("D M j G:i:s T Y") . "\n";     print_r($_SERVER);     print_r($_POST);     $body = ob_get_clean();     file_put_contents("/logs/incomingPayments/IPN.txt", $body, FILE_APPEND); 

Just in case things go awry, it is a good idea to get a record of the request before anything else happens.

   //Step 1. Verify IPN With PayPal   $result=verifyIPN($_POST);   if ($result == 0)   {     $subject = "FAKE IPN RECEIVED";     $address = "security@example.com";     $headers =        "From: ipn_processor@example.com\r\n" .        "Reply-To: donotreply@example.com\r\n" .        "X-Mailer: PHP/" . phpversion();     mail($address, $subject, $body, $headers);     exit; 

If the IPN is reported as being invalid by PayPal, notify the appropriate groups of the attempted transaction.

   }else if($result != 1)   {     $subject = "Unable to validate IPN";     $body = "If this payment notification is valid it will need to be         manually processed\n $result\n $body";     $address = "support@example.com";     $headers =        "From: ipn_processor@example.com\r\n" .        "Reply-To: donotreply@example.com\r\n" .        "X-Mailer: PHP/" . phpversion();     mail($address, $subject, $body, $headers);     exit;   } 

This will occur if, for whatever reason, the script is unable to contact PayPal to confirm the transaction, and it will email the customer regarding the transaction. It will need to be confirmed (manually check to ensure it exists in PayPal's transaction database), then processed manually.

   //Step 1.5 Check payment status   switch ($_POST[‘payment_status'])   {     case "Completed":       break;     case "Pending":       paymentPendingThankYou($_POST[‘payer_email']);       break;     default:       $body = "Hi, an IPN was received that wasn't a completed payment or         a pending payment. Please confirm this transaction against our records.";       $body .= $post;       $subject = "IPN Received";       $address = "support@example.com";       $headers =         "From: ipn_processor@example.com\r\n" .         "Reply-To: donotreply@example.com\r\n" .         "X-Mailer: PHP/" . phpversion();       mail($address, $subject, $body, $headers);       exit;     break;   } 

If the payment is still Pending, the customer is emailed and informed of the status. Remember that several PayPal payment methods (such as eChecks) can take several business days to clear. The paymentPendingThankYou() function is presented shortly. If the payment status isn't Pending or Completed, it will likely be Failed (or something worse, like Reversed), in which case customer service is notified. In a case where a charge back was received, customer service may need to investigate or follow up with the customer.

   //Step 2. Confirm Product Information   $result = confirmProduct($_POST[‘item_number'],      $_POST[‘item_name'],$_POST[‘payment_gross']);   if ($result == false)   {     $subject = "Product Name/ID/Price mis-match";     $address = "support@example.com";     $headers =        "From: ipn_processor@example.com\r\n" .        "Reply-To: donotreply@example.com\r\n" .        "X-Mailer: PHP/" . phpversion();     mail($address, $subject, $body, $headers);     exit;   } 

Here the order is checked against the available product database. If it turns out that the product and price point do not exist in the database, notify customer service. Depending on the response time of customer service (remember that orders may be received any time day or night, holidays and all), you may want to send the customer an email acknowledging the order and letting them know it will be processed manually.

   //Step 3. Process the order   processOrder($_POST);   exit; 

Finally, the order is dispatched for processing.

 function paymentPendingThankYou($address) {   $subject = "Order Received";   $body = "Thanks for your order with Example.com!\n     This message confirms that we have received notification from     PayPal regarding your order. However, PayPal is still processing     your payment at this time. Once PayPal confirms that they have completed     processing your payment we will contact you again to confirm payment and     include shipping details.\n\n     If you have any questions please do not hesitate to contact us at support@example.com.\n\n     We appreciate your business!";   $headers =      "From: OrderConfirmation@example.com\r\n" .      "Reply-To: support@example.com\r\n" .      "X-Mailer: PHP/" . phpversion();   mail($address, $subject, $body, $headers);   exit; } 

When a payment is received with Pending status, a quick email is sent to the client informing them of the status of their order.

 function verifyIPN($data) { } function confirmProduct($id, $name, $amount) { } 

Code shown above, omitted for brevity.

 function processOrder($data) {  // Your processing code goes here } 

This code is completely case specific, so you're on your own.

 ?> 

As you can tell, PayPal's Instant Payment Notification system gives merchants an easy and secure manner to accept payments in an automated manner. While the implementation may seem backwards compared to other APIs presented (in that PayPal contacts you, rather than the other way around), this change presents few additional challenges.




Professional Web APIs with PHP. eBay, Google, PayPal, Amazon, FedEx, Plus Web Feeds
Professional Web APIs with PHP. eBay, Google, PayPal, Amazon, FedEx, Plus Web Feeds
ISBN: 764589547
EAN: N/A
Year: 2006
Pages: 130

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net