How to integrate PayPal into PHP

dcblog

David Carr

Posted on May 8, 2020

How to integrate PayPal into PHP

How to integrate PayPal into PHP

In this post, I’ll explain how to use PayPal for one-off payment and how to use the Instant Payment Notification (IPN) to process payments on your website.

When testing you don’t want to actually spend any money, this is where sandbox accounts come in. Sandbox accounts lets you simulate transactions without actually spending money everything is simulated. The sandbox accounts lets you run through the entire transaction when it comes time to change to real accounts its a case of changing the account and URLs used, a quick change to make.

Sandbox Accounts

Sign in to your PayPal account into PayPal Developer Sandbox accounts - PayPal Developer

Under Sandbox in the left sidebar click on Accounts, here you will see your automatically generated sandbox accounts if you don’t have any click create an account.

The default accounts are 1 personal and 1 business. These are perfect for testing transactions with PayPal.

You use the personal account to test payments to the business account. To get the password for each click on the ellipse on the far right and click on view/edit account.

PayPal Form

To send payment data to a PayPal checkout page you can use a form, there’s a lot of details on PayPal’s docs how do-i-add-paypal-checkout-to-my-custom-shopping-cart

The form posts to of https://www.sandbox.paypal.com/cgi-bin/webscr for sandbox accounts or for normal payments use https://www.paypal.com/cgi-bin/webscr

If you are sending multiple items then use the cart options:

<input type="hidden" name="cmd" value="_cart" />
<input type="hidden" name="upload" value="1" />
Enter fullscreen mode Exit fullscreen mode

For single items only then use

<input type=“hidden” name=“cmd” value="_xclick">
Enter fullscreen mode Exit fullscreen mode

Having said this I default to using cart it can be used to single and multiple items so keep your code consistent.

To link the payment your PayPal business account enter the account email address to:

<input type="hidden" name="business" value="email@business.example.com" />
Enter fullscreen mode Exit fullscreen mode

Core form data:

<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post" />
<input type="hidden" name="cmd" value="_cart" />
<input type="hidden" name="upload" value="1" />
<input type="hidden" name="business" value="email@business.example.com" />
Enter fullscreen mode Exit fullscreen mode

Additional options:

Set the currency:

<input type="hidden" name="currency_code" value="GBP" />
<input type="hidden" name="lc" value="UK" />
<input type="hidden" name="rm" value="2" />
Enter fullscreen mode Exit fullscreen mode

Set a return url, this will be where the user is directed to after completing a payment.

The cancel url will be where the user is directed after canceling a payment.

The notify url is where Instant Payment Notifications (IPN) are sent to after payment

<input type="hidden" name="return" value="https://domain.com/thankyou" />
<input type="hidden" name="cancel_return" value="https://domain.com/cancel" />
<input type="hidden" name="notify_url" value="https://domain.com/ipn" />
<input type="hidden" name="charset" value="utf-8" />
Enter fullscreen mode Exit fullscreen mode

Adding items

Each item will have a number on the end for example imte_name_1 is the first item another item would have item_name_2 and so on:

<input type="hidden" name="item_name_1" value="PC" />
<input type="hidden" name="item_number_1" value="0045" />
<input type="hidden" name="amount_1" value="500.00" />
<input type="hidden" name="quantity_1" value="1" />
Enter fullscreen mode Exit fullscreen mode

For the pay button you can use PayPal’s image:

<input style="margin-top:10px;" type="image" src="http://www.paypal.com/en_US/i/btn/x-click-but01.gif" name="submit" alt="Make payments with PayPal - it's fast, free and secure!">
</form>
Enter fullscreen mode Exit fullscreen mode

Putting it all together:

<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post" />
<input type="hidden" name="cmd" value="_cart" />
<input type="hidden" name="upload" value="1" />
<input type="hidden" name="business" value="email@business.example.com" />
<input type="hidden" name="currency_code" value="GBP" />
<input type="hidden" name="lc" value="UK" />
<input type="hidden" name="rm" value="2" />
<input type="hidden" name="return" value="https://domain.com/thankyou" />
<input type="hidden" name="cancel_return" value="https://domain.com/cancel" />
<input type="hidden" name="notify_url" value="https://domain.com/ipn" />
<input type="hidden" name="charset" value="utf-8" />
<input type="hidden" name="item_name_1" value="PC" />
<input type="hidden" name="item_number_1 value="0045" />
<input type="hidden" name="amount_1" value="500.00" />
<input type="hidden" name="quantity_1" value="1" />
<input style="margin-top:10px;" type="image" src="http://www.paypal.com/en_US/i/btn/x-click-but01.gif" name="submit" alt="Make payments with PayPal - it's fast, free and secure!">
</form>
Enter fullscreen mode Exit fullscreen mode

Whilst this works it does open it up to abuse, anyone can inspect the form and change any of the fields such as the price before it goes to PayPal!

What I prefer to do it send the data directly from a PHP page, this way it cannot be modified before being sent to PayPal.

Post data to PayPal

The same form data is still used with this approach only it’s not laid out in a form but instead in an array:

$fields = [
    'cmd'           => '_cart',
    'upload'        => '1',
    'business'      => 'email@business.example.com',
    'currency_code' => 'GBP',
    'lc'            => 'UK',
    'rm'            => '2',
    'return'        => 'https://domain.com/thankyou',
    'cancel_return' => 'https://domain.com/cancel',
    'notify_url'    => 'https://domain.com/ipn'
];

//add items
$fields["item_name_1"]   = 'PC';
$fields["item_number_1"] = '0045';
$fields["amount_1"]      = '500.00';
$fields["quantity_1"]    = 1;

$fields["item_name_2"]   = 'Office Chair';
$fields["item_number_2"] = '0098';
$fields["amount_2"]      = '60.00';
$fields["quantity_2"]    = 1;

// Prepare query string
$query_string = http_build_query($fields);

header('Location: https://www.sandbox.paypal.com/cgi-bin/webscr?' . $query_string);
exit();
Enter fullscreen mode Exit fullscreen mode

This will send the array data to the URL as a POST request.

To send a custom item such as an user_id you can use a custom field:

$fields['custom'] = $user_id;
Enter fullscreen mode Exit fullscreen mode

Or part of the initial fields array:

$fields = [
    'cmd'           => '_cart',
    'upload'        => '1',
    'business'      => 'email@business.example.com',
    'currency_code' => 'GBP',
    'lc'            => 'UK',
    'rm'            => '2',
    'return'        => 'https://domain.com/thankyou',
    'cancel_return' => 'https://domain.com/cancel',
    'notify_url'    => 'https://domain.com/ipn',
    'custom'        => $user_id
];
Enter fullscreen mode Exit fullscreen mode

Remember to change the sandbox URL and business field when switching to a live account.

For Laravel users, the IPN will be POSTED to your notify_url endpoint. It will not have a CSRF token to make sure you exclude the call from CSRF token checks by going to app/Http/Middleware/VerifyCsrfToken.php and adding the endpoint to the $except array:

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'ipn'
    ];
}
Enter fullscreen mode Exit fullscreen mode

Handling IPN

Once payment has been made an IPN is sent to the specified notify_url.

First, specify weather to use sandbox with a Y for yes or anything else for No.

Collect the POST data

// Read POST data
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
    $keyval = explode ('=', $keyval);
    if (count($keyval) == 2){
        $myPost[$keyval[0]] = urldecode($keyval[1]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Read the post from PayPal and add CMD

$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
    $value = urlencode($value);
    $req .= "&$key=$value";
}
Enter fullscreen mode Exit fullscreen mode

Set the paypal URL

if (USE_SANDBOX == 'Y') {
    $paypal_url = "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr";
} else {
    $paypal_url = "https://ipnpb.paypal.com/cgi-bin/webscr";
}
Enter fullscreen mode Exit fullscreen mode

Next to confirm the payment a request is sent back to PayPal

$ch = curl_init($paypal_url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
$res = curl_exec($ch);
Enter fullscreen mode Exit fullscreen mode

If there is an error connecting throw an exception

if (curl_errno($ch) != 0) {

    throw new \Exception("Can't connect to PayPal to validate IPN message: ".curl_error($ch)); 

    return true;
}
Enter fullscreen mode Exit fullscreen mode

A successful payment will have VERIFIED in the PayPal payload so check for that:

if (preg_match("/VERIFIED/", $res)) {
//payment complete, process order & send an email etc
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together:

define("USE_SANDBOX", 'Y');

// Read POST data
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
    $keyval = explode ('=', $keyval);
    if (count($keyval) == 2){
        $myPost[$keyval[0]] = urldecode($keyval[1]);
    }
}
// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
    $value = urlencode($value);
    $req .= "&$key=$value";
}
if (USE_SANDBOX == 'Y') {
    $paypal_url = "https://ipnpb.sandbox.paypal.com/cgi-bin/webscr";
} else {
    $paypal_url = "https://ipnpb.paypal.com/cgi-bin/webscr";
}
$ch = curl_init($paypal_url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
$res = curl_exec($ch);

if (curl_errno($ch) != 0) {

    throw new \Exception("Can't connect to PayPal to validate IPN message: ".curl_error($ch)); 

    return true;
} else {

    if (preg_match("/VERIFIED/", $res)) {
        //payment complete, you should send en email here.
    } else {
        //IPN not verified, you should send en email here.
    }
}
curl_close($ch);
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
dcblog
David Carr

Posted on May 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related