PayPal Checkout with Smart Payment Buttons

PayPal is globally used electronic payment processor. People can shop online, transfer money online and receive money online using a single PayPal account. Because of its features and global reach PayPal has become a "must be included" payment method for online shopping stores.

Paypal Checkout with Smart Payment Buttons

Note: This post was initially published with the use of PayPal's Checkout-PHP-SDK Package. Since that package had been archived on Jul 21, 2022 this post has been updated to use REST API requests as recommended by Paypal.

PayPal had provided API integrations for online shopping stores to receive payments for placed orders. Recently PayPal has updated orders API and introduced Smart Payment Buttons for checkout and recommends using this new API integration which uses Javascript SDK in combination with Orders  API v2. This Smart Payment Buttons is valid from Feb, 2019. PayPal is not going to update old checkout API which uses checkout.js for new features and enhancements. So it is recommended to use the new checkout integration using Smart Payment Buttons.

How do Smart Payment Buttons work?

  1. You place the buttons on checkout page on your website.
  2. Buyer clicks the buttons to pay for order.
  3. Buttons launch the checkout experience.
  4. Buyer makes the payment and Orders API is called to finalize the transaction.
  5. You show order confirmation to buyer upon successful transaction.

How to setup PayPal Smart Payment Buttons?

Smart Payment Buttons can be either client side integration or server side integration. We will implement these buttons using server side approach as it does not reveal much information to client and we can call PayPal's order api from our server. You can refer to PayPal's Call Orders API from Your Server for use cases which request you are supposed to make depending upon your requirements. Lets break it into following steps:

  • We will need a PayPal sandbox account.
  • We will use OAuth 2.0 client ID and client secret of this sandbox account.
  • We will implement Payment Buttons using Javascript SDK.
  • Setup our server to make calls to PayPal's Orders API v2.
  • We will call PayPal Orders API from our server, to create an order and capture the transaction.

Before we get started, For demonstration I have created a session to hold cart products and amount statically. I shall not be saving order to database after successful transaction and I am under opinion you already have a cart setup. So lets get started

Create a Sandbox App and Get Rest Credentials

  • Login to your PayPal developer account and navigate to PayPal Developer
  • Click on Sandbox Accounts under Testing Tools top navigation menu item.
  • Click on Create Account button, We need two accounts one for buyer and other for merchant. You don't need to create these accounts if you already have them created.
  •  Now click on App & Credentials in top navigation menu.
  • Click on Create App and enter App name and assign a merchant account to App.

    Paypal App & Credentials
  • After you have created your app you will see a screen like this copy Client ID and Client Secret.

    Paypal App & Credentials

Now that we have our Sandbox Client ID & Secret, we are ready to setup our Smart Payment Buttons. First install PayPal Server SDK using following command in composer.

composer require paypal/paypal-checkout-sdk 1.0.0

We are going to create following files in the directory where our checkout cart is placed.

  • constants.php 
  • paypal-sdk/paypal_client.php
  • index.php
  • javascript.js
  • paypal-request.php
  • style.css

constants.php

<?php
define('PAYPAL_ENVIRONMENT', 'sandbox');
define('PAYPAL_CLIENT_ID', 'PAYPAL_CLIENT_ID');
define('PAYPAL_CLIENT_SECRET', 'PAYPAL_CLIENT_SECRET');

paypal_client.php

A custom PHP class to send requests to PayPal endpoints. Class has the following properties in use:

  • $base_url: Holds the endpoint URL for sandbox or production.
  • $client_id: Client id created in previous steps.
  • $client_secret: Client secret created in previous steps.
  • $access_token: Access token for PayPal requests authorization.
  • $token_type: The type of token i.e "Bearer".
  • $expires_in: The expire time of current access token. Used to refresh/recreate the token when expired.
  • $created_at: The creation time of current access token. Used to refresh/recreated the token when expired.
The methods used in paypal_client class:

__construct: Constructor of class when and instance of class is created. Sets $client_id, $client_secret $base_url and $access_token properties of the class.

set_access_token: Sends the request to PayPal's authorization endpoint to retrieve access token for future requests. Sets $access_token, $token_type, $expires_in and $created_at properties of class.

curl_requests: A generic function used to send curl requests to PayPal endpoints.

<?php

class paypal_client{
public string $base_url;

private string $client_id;

private string $client_secret;

private ?string $access_token;

private string $token_type;

private int $expires_in;

private int $created_at;

public function __construct($client_id, $client_secret, $sandbox = false)
{
$this->client_id = $client_id;

$this->client_secret = $client_secret;

$this->base_url = $sandbox
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';

$this->access_token = null;
}


/**
* @throws Exception
*/
public function set_access_token() :stdClass|array
{
$headers = [
'content-type' => 'application/x-www-form-urlencoded'
];

$curl_options = [
CURLOPT_USERPWD => $this->client_id . ':' . $this->client_secret
];

$body = [
'grant_type' => 'client_credentials'
];

$response = $this->curl_request('/v1/oauth2/token', 'POST', $headers, http_build_query($body), $curl_options);

$response = json_decode($response);

$this->access_token = $response->access_token;

$this->token_type = $response->token_type;

$this->expires_in = $response->expires_in;

$this->created_at = time();

return $response;
}


/**
* @param string $path
* @param string $method
* @param array $headers
* @param array|string $body
* @param array $curl_options
* @return bool|string
* @throws Exception
*/
public function curl_request(string $path, string $method, array $headers = [], array|string $body = [], array $curl_options = []): bool|string
{
$curl = curl_init();

array_change_key_case($headers);

$headers = array_merge(['accept' => 'application/json'], $headers);

curl_setopt($curl, CURLOPT_URL, $this->base_url . $path);
curl_setopt($curl, CURLOPT_TIMEOUT, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

if(str_starts_with($this->base_url, 'https://')){
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
}

if(!array_key_exists('authorization', $headers) && !str_ends_with($path, 'v1/oauth2/token')){
if (is_null($this->access_token) || time() > ($this->created_at + $this->expires_in)){
$this->set_access_token();
}

$headers['authorization'] = sprintf('%s %s', $this->token_type, $this->access_token);
}

// If any headers set add them to curl request
if(!empty($headers)){
curl_setopt($curl, CURLOPT_HTTPHEADER, array_map(function($key, $value){
return $key . ': '. $value;
}, array_keys($headers), array_values($headers)));
}

// Set the request type , GET, POST, PUT or DELETE
switch(strtoupper($method)){
case 'POST':
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
break;
case 'PUT':
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
break;
case 'DELETE':
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
default:
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
break;
}

// If any data is supposed to be sent along with request add it to curl request
if(!empty($body)){
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
}

// Any extra curl options to add in curl object
if(!empty($curl_options)){
foreach($curl_options as $option_key => $option_value){
curl_setopt($curl, $option_key, $option_value);
}
}

$response = curl_exec($curl);

$error = curl_error($curl);

$error_code = curl_errno($curl);

$status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);

if($error_code > 0){
throw new Exception($error, $error_code);
}

if ($status_code < 200 || $status_code >= 300) {
throw new Exception($response, $status_code);
}

curl_close($curl);

return $response;
}
}

index.php

Contains our checkout cart, I have created a static array of products for demonstration. Next loop through products array to calculate total tax amount and total items amount (without tax). Then I created a cart session that can be accessed in our PayPal SDK.

<?php
session_start();

include __DIR__ . '/constants.php';

$products = [];

// Static array of products
$products[] = ['product_title' => 'Javascript Book - PDF', 'product_price' => 10.00, 'product_tax' => 2.00, 'product_quantity' => 3];
$products[] = ['product_title' => 'PHP Book - PDF', 'product_price' => 15.00, 'product_tax' => 3.00, 'product_quantity' => 2];

$items_total = $tax_total = 0.00;

// Loop through products and calculate tax total & items total
foreach($products as $product){
$tax_total += floatval($product['product_tax'] * $product['product_quantity']);
$items_total += floatval($product['product_price'] * $product['product_quantity']);
}

// Function to show amount in price format
function to_price($amount = 0.00, $locale = 'en_US', $currency_code = 'USD'){
return numfmt_format_currency(numfmt_create( $locale, NumberFormatter::CURRENCY ), $amount, $currency_code);
}

// Create a cart session
$_SESSION['cart']['products'] = $products;
$_SESSION['cart']['items_total'] = $items_total;
$_SESSION['cart']['tax_total'] = $tax_total;
$_SESSION['cart']['cart_total'] = $tax_total + $items_total;
?>
<!DOCTYPE html>
<html>
<head>
<title>PayPal Checkout with Smart Payment Buttons - Demo</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<script defer="defer" src="https://www.paypal.com/sdk/js?client-id=<?=PAYPAL_CLIENT_ID?>"></script>
<script defer="defer" src="js/javascript.js"></script>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class="container">
<div class="my-4">
<table class="table table-bordered mb-4">
<thead>
<tr>
<th colspan="2">Product</th>
<th style="width: 120px">Price</th>
<th style="width:120px;">Tax</th>
<th style="width:100px;">Quantity</th>
<th style="width: 120px;">Item Total</th>
</tr>
</thead>
<tbody>
<?php foreach($_SESSION['cart']['products'] as $product){
$item_total = $product['product_price'] * $product['product_quantity'];
$item_tax = $product['product_tax'] * $product['product_quantity'];
?>
<tr>
<td width="31">
<img src="images/pdf-book.png" />
</td>
<td>
<?=$product['product_title'];?>
</td>
<td>
<?=to_price($product['product_price']);?>
</td>
<td>
<?=to_price($product['product_tax']);?>
</td>
<td>
<?=$product['product_quantity']?>
</td>
<td>
<?=to_price($item_total + $item_tax);?>
</td>
</tr>
<?php }?>
<tr>
<th colspan="5" class="text-right">
Tax Total
</th>
<th class="text-right">
<?=to_price($_SESSION['cart']['tax_total']);?>
</th>
</tr>
<tr>
<th colspan="5" class="text-right">
Items Total
</th>
<th class="text-right">
<?=to_price($_SESSION['cart']['items_total']);?>
</th>
</tr>
<tr>
<th colspan="5" class="text-right">
Cart Total
</th>
<th class="text-right">
<?=to_price($_SESSION['cart']['cart_total']);?>
</th>
</tr>
</tbody>
</table>
<div id="paypal-buttons"></div>
</div>
</div>
</body>
</html>

javascript.js

Renders smart payment buttons, "createOrder" initiates a call to our server side SDK to create an order. "onApprove" is invoked when transaction is successful and we redirect to a thank you/success page after successful transaction.
window.addEventListener("load", function(){
// Render paypal Buttons
paypal.Buttons({
style:{
layout: "horizontal"
},
// Call your server to create an order
createOrder: function(data, actions) {
return fetch("paypal-request.php", {
mode: "no-cors",
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
action: "create-order",
})
}).then(function(res) {
return res.json();
}).then(function(data) {
if( !data.success && data.message ){
console.error(data.message);
}
return data.id;
});
},
// Call your server to save the transaction
onApprove: function(data, actions){
return fetch("paypal-request.php", {
mode: "no-cors",
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
action: "save-order",
id: data.orderID
})
}).then(function(res) {
return res.json();
}).then(function(data){
// Redirect to thank you/success page after saving transaction
if(data.success){
window.location.assign("payment-success.php");
}
});
}
}).render("#paypal-buttons");
});

paypal-request.php

Uses paypal_client class to create and order in PayPal and then capture it after successful payment.
<?php
session_start();

include __DIR__ . '/constants.php';

// Import the PayPal SDK client that was created in 'Set up Server-Side SDK'.
include __DIR__ . '/paypal-sdk/paypal_client.php';

// Capture the input received from fetch() request
$json_data = file_get_contents('php://input');
$request = json_decode($json_data);

$paypal_client = new paypal_client(PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_ENVIRONMENT === 'sandbox');

switch($request->action){
case 'create-order':
$headers = [
'prefer' => 'return=representation',
'content-type' => 'application/json'
];

// Assign cart from session to a variable
$cart = $_SESSION['cart'];

// Set currency code used in transaction
$currency_code = 'USD';

$request_body = [];
$application_context = [];
$purchase_units = [];
$pu_amount = [];

// Prepare context of our request
$application_context['locale'] = 'en-US';
$application_context['user_action'] = 'PAY_NOW';

// Prepare amount & break down of amount for order
$pu_amount['currency_code'] = $currency_code;
$pu_amount['value'] = floatval($cart['cart_total']);

// Items total break down without tax
$pu_amount['breakdown']['item_total']['currency_code'] = $currency_code;
$pu_amount['breakdown']['item_total']['value'] = number_format($cart['items_total'], 2);

// Total tax of all products
$pu_amount['breakdown']['tax_total']['currency_code'] = $currency_code;
$pu_amount['breakdown']['tax_total']['value'] = number_format($cart['tax_total'], 2);

$purchase_units['amount'] = $pu_amount;

$items = [];

// Loop through all products in session and prepare items array for order request
if( !empty($cart['products']) ){
foreach($cart['products'] as $product){
$item['name'] = $product['product_title'];
$item['quantity'] = $product['product_quantity'];
$item['category'] = 'DIGITAL_GOODS';

// Unit amount of product (without tax)
$item['unit_amount']['currency_code'] = $currency_code;
$item['unit_amount']['value']= number_format($product['product_price'], 2);

$items[] = $item;
}
}

$purchase_units['items'] = $items;

// Finally create request body array assigning context & purchase units created above
$request_body['intent'] = 'CAPTURE';
$request_body['application_context'] = $application_context;
$request_body['purchase_units'][] = $purchase_units;

try {
$response = $paypal_client->curl_request('/v2/checkout/orders', 'POST', $headers, json_encode($request_body));
die($response);
} catch (Exception $e) {
echo $e->getMessage();
}

break;
case 'save-order':
$headers = [
'prefer' => 'return=minimal',
'content-type' => 'application/json'
];

try {
$response = $paypal_client->curl_request(sprintf('/v2/checkout/orders/%s/capture', $request->id), 'POST', $headers);

// Save order to database here

// unset cart session
unset($_SESSION['cart']);

echo json_encode(['success' => 1], JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo $e->getMessage();
}

break;
}

style.css

*{
box-sizing: border-box;
}
html,body{
margin: 0;
padding: 0;
}
body{
background-color: #f6f6f6;
font-family: "Segoe UI", "Roboto", "Helvetica", sans-serif;
font-size: 15px;
font-weight: normal;
font-style: normal;
line-height: 1.5;
}
.container{
width: 100%;
max-width: 1140px;
margin-right: auto;
margin-left: auto;
padding-right: 15px;
padding-left: 15px;
}
.my-4{
margin-top: 1rem;
margin-bottom: 1rem;
}
.text-right{
text-align: right;
}
.table{
width: 100%;
border-collapse: collapse;
background-color: #fff;
margin-bottom: 1rem;
}
.table tr th,
.table tr td{
border: 1px solid #dddddd;
padding: 10px;
}
#paypal-buttons{
max-width: 750px;
margin-right: auto;
margin-left: auto;
}

Login to your sandbox buyer or merchant account and verify transaction was placed. You would see transaction details like in screenshot below:

Paypal Transaction Details
You can modify the code to avoid second request for creating order in database.