PayPal Payouts API Integration in PHP

In previous post we demonstrated how to implement PayPal checkout experience using smart payment buttons. PayPal Payouts allows you to send payments to multiple recipients at the same time.

Paypal Payouts API Integration in PHP

With PayPal payout you can send payments to multiple recipients as a batch. There are three types of payouts PayPal provides.

  1. Large Batch Payout: Create a payouts file and upload it to secure FTP server. Number of recipients is unlimited.
  2. API Integration: Make programming calls to payout API. Number of recipients is up to 15000 per request.
  3. Payouts Web: Create a payouts file and upload it using Payouts Web. Number of recipients is 5000 per file.

We will be using API Integration to programmatically send payments to multiple receivers at a time. Before you can send payout requests to payouts API you must have:

  • A PayPal account.
  • Client ID and Client Secret. 
  • Sufficient funds in your account.
  • Payout feature must be enabled. 

First setup a sandbox account and create an App. After your app is created get your client ID and client secret, next under "SANDBOX APP SETTINGS" enable payouts options to allow your app to send requests to PayPal Payouts API. You can follow instructions for creating an app and getting your client ID & Secret.

I have divided the payouts integration into three basic steps:

  1. A function to send CURL requests.
  2. A request to get access token.
  3. Final request to send payouts.

Before you send any kind of resource request to PayPal you need an access token to authenticate your request. We will be sending two requests one to get an access token and other to send payouts so we will first write a function that can be used in both requests. To get an access token we need our client ID and Client Secret.


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;
}
}

Payouts Request

<?php
// Headers for our token request
$headers['accept'] = 'application/json';
$headers['content-type'] = 'application/json';

$request_body = [];
$items = [];

$time = time();
// Prepare sender batch header
$sender_batch_header['sender_batch_id'] = $time;
$sender_batch_header['email_subject'] = 'Payout Received';
$sender_batch_header['email_message'] = 'You have received a payout, Thank you for using our services';

// First receiver
$receiver['recipient_type'] = 'EMAIL';
$receiver['note'] = 'Thank you for your services';
$receiver['sender_item_id'] = $time++;
$receiver['receiver'] = '[email protected]';
$receiver['amount']['value'] = 10.00;
$receiver['amount']['currency'] = 'USD';

$items[] = $receiver;

// Second receiver
$receiver['recipient_type'] = "EMAIL";
$receiver['note'] = 'You received a payout for your services';
$receiver['sender_item_id'] = $time++;
$receiver['receiver'] = '[email protected]';
$receiver['amount']['value'] = 15.00;
$receiver['amount']['currency'] = 'USD';

$items[] = $receiver;

$request_body['sender_batch_header'] = $sender_batch_header;
$request_body['items'] = $items;

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

$response = $paypal_client->curl_request('/v1/payments/payouts', 'POST', $headers, json_encode($request_body));
?>

For demo purpose I used static array of receivers, you can prepare your receivers list from database table or file. PayPal prevents duplicate payouts if sender_batch_id has been already used in past 30 days PayPal will reject the payout request and return an error. Below is the successful payout response.
{
  "batch_header": {
    "sender_batch_header": {
      "sender_batch_id": "2014021801",
      "email_subject": "You have a payout!"
    },
    "payout_batch_id": "12345678",
    "batch_status": "PENDING"
  }
}