Loading, please wait...

picture of me
Joe Rushton Fullstack Web Developer

Setting up PayPal Recurring Payments in PHP

03rd May 2017 Reading Time: 10 mins
Tags: PHP

I've recently had the pleasure of integrating a recurring payment system (or subscription model if you like), into an existing web application and thought it might be worthwhile to write down the process.

Recurring payments can be a little tricky because you have to deal with cancellations and renewals but I've broken it down into the following steps:

1) Setting up your PayPal Developer application

Before we start doing anything, head over to the PayPal Developer Section and create yourself a new REST app from within the "My Apps & Credentials" section. From this you can acquire a Client ID and a Secret which you will need shortly.

Now we want to connect to our application in PHP. To do so we require the paypal SDK for php. If you're familiar with composer, run:

composer require "paypal/rest-api-sdk-php:*"

from inside your project directory to grab the latest version, otherwise pop over to here to manually install. Don't forget to include the composer autoload file if you use the composer method.

Now lets connect:

$paypal = new \PayPal\Rest\ApiContext(
	new \PayPal\Auth\OAuthTokenCredential(
		'YOUR-CLIENT-ID',
	        'YOUR-SECRET'
	)
);

I found it useful to put the context instanciation into a helper function as it's required in more than one script.

2) Creating the subscription

Next on the agenda are a couple of prelimary steps to take before any money exchanges hands. First we need to configure a "plan" and set all of the required options:

$plan = new \PayPal\Api\Plan();

$plan->setName('Some recurring membership')
		->setDescription('Put your description here')
		->setType('INFINITE');

$paymentDefinition = new \PayPal\Api\PaymentDefinition();
$paymentDefinition->setName('Regular Payments')
	    ->setType('REGULAR')
	    ->setFrequency('Month')
	    ->setFrequencyInterval("1")
	    ->setAmount(new \PayPal\Api\Currency(array('value' => 5.99, 'currency' => 'GBP')));

$merchantPreferences = new \PayPal\Api\MerchantPreferences();

$merchantPreferences->setReturnUrl('example.com/index.php?success=true')
	    ->setCancelUrl('example.com/index.php?success=false')
	    ->setAutoBillAmount("yes")
	    ->setInitialFailAmountAction("CONTINUE")
	    ->setMaxFailAttempts("0")
	    ->setSetupFee(new \PayPal\Api\Currency(array('value' => 5.99, 'currency' => 'GBP')));

$plan->setPaymentDefinitions(array($paymentDefinition));
$plan->setMerchantPreferences($merchantPreferences);

Something not so obvious about recurring payments is that the first payment doesn't actually go out straight away.. my workaround for that was to set the setup fee equal to the monthly subscription cost and then later we can adjust the first payment to go out in exactly 1 months time (setup fees are paid immediately). Now create the plan:

try {
	$createdPlan = $plan->create($paypal);
} catch (Exception $ex) {
	debug($ex->getMessage());
	die();
}

It's also a requirement to update the state of the newly created plan to active like so:

try {
    $patch = new \PayPal\Api\Patch();

    $value = new \PayPal\Common\PayPalModel('{
       "state":"ACTIVE"
     }');

    $patch->setOp('replace')
        ->setPath('/')
        ->setValue($value);
    $patchRequest = new \PayPal\Api\PatchRequest();
    $patchRequest->addPatch($patch);

    $createdPlan->update($patchRequest, $paypal);

    $createdPlan = \PayPal\Api\Plan::get($createdPlan->getId(), $paypal);
} catch (Exception $ex) {
		debug($ex->getMessage());
    die();
}

Now to create an agreement, set the start date and link it up to our plan (don't worry if you're getting the different classes confused, just bare with):

$agreement = new \PayPal\Api\Agreement();

$agreement->setName('Some subscription name')
    ->setDescription('Initial payment of £5.99 followed by a recurring payment of £5.99 on the ' . date('jS') . ' of every month.')
    // set the start date to 1 month from now as we take our first payment via the setup fee
    ->setStartDate(gmdate("Y-m-d\TH:i:s\Z", strtotime("+1 month", time())));

// Link the plan up with the agreement
$plan = new \PayPal\Api\Plan();
$plan->setId($createdPlan->getId());
$agreement->setPlan($plan);

 

3) Send the user off to PayPal

We can finally take the payment! (well after we've set the payment method):

// Add Payer
$payer = new \PayPal\Api\Payer();
$payer->setPaymentMethod('paypal');
$agreement->setPayer($payer);

Execute the agreement and grab approval url

try {
    // Please note that as the agreement has not yet activated, we wont be receiving the ID just yet.
    $agreement = $agreement->create($paypal);

    // Get redirect url
    $approvalUrl = $agreement->getApprovalLink();
} catch (Exception $ex) {
	debug($ex->getMessage());
	die();
}

And finally, send the user to PayPal:

header("Location: {$approvalUrl}");

 

4) Handling Success/Failed Payments

So far we've setup a subscription and the user has been sent off to PayPay but we're not quite done yet. If you were paying attention, you'd have noticed that we set a cancel and return URL earlier. These could go to two separate scripts but in my example I simply return back to index.php with a GET parameter of success - either true or false depending on paypals feedback.

NB: we haven't actually taken the payment at this point. The setup fee goes out when the agreement is "executed".

The last step of the payment process happens next when the user is redirected to your success URL along with a GET token. E.g.

example.com/index.php?success=true&token=123456

Inside index.php we need to check the value of success and the presence of a token so we can confirm the agreement:

if (!empty($_GET['success'])) {
	$success = $_GET['success'];

	if ($success && !empty($_GET['token'])) {
		// paypal was successful but the payment still needs processing
		$token = $_GET['token'];

		$agreement = new \PayPal\Api\Agreement();
	    try {
			// Execute the agreement by passing in the token
			$agreement->execute($token, $paypal);
	    } catch (Exception $ex) {

	    }
        // Record the transaction here.

        // Upgrade the user account, etc.
	} else {
		// payment failed, perhaps send the user elsewhere and log the error
	}
}

How you process the after-sale will depend on your use-case but you will most likely want to store the transaction in a database, update the status of a user record etc. I store for following values in a transactions table:

Agreement ID: $agreement->getId();

Next Billing Date: $agreementDetails = $agreement->getAgreementDetails();
                   $agreementDetails->getNextBillingDate();

Start Date: gmdate("Y-m-d\TH:i:s\Z")

Token: $_GET['token']

 

5) Handling Cancellations and Renewals with Webhooks

In order for your application to recognize any changes to your newly created subscription (i.e. cancellations/renewals), we will need to set up a webhook. You can get a full explanation of webhooks here but in short, they send notifications to a specified URL and contain information on a particular event. Don't get webhooks confused with Instant Payment Notifications (IPN). They both serve the same purpose but IPNs are for the classic API and we are working with the REST api.

 

First you want to setup a webhook by going to your application (https://developer.paypal.com/developer/applications), click "Add Webhook" and select All events under the events tracked column. For the URL enter yourdomain.com/paypal_webhooks.php.

Now create the file paypal_webhooks.php and ensure the SDK files are included. This file is called a webhook listener:

<?php
require_once('./vendor/autoload.php');

use \PayPal\Api\VerifyWebhookSignature;
use \PayPal\Api\WebhookEvent;

 Then instanciate the paypal api as we did earlier, and grab the request headers and body:

$apiContext = getPaypalContext();

/**
* Receive the entire body that you received from PayPal webhook.
*/
$bodyReceived = file_get_contents('php://input');

/**
* Receive HTTP headers that you received from PayPal webhook.
*/
$headers = getallheaders();

/**
* Uppercase all the headers for consistency
*/
$headers = array_change_key_case($headers, CASE_UPPER);

Now we need to validate the webhook, be sure to enter your webhook id (http://paypal.github.io/PayPal-PHP-SDK/sample/doc/notifications/ValidateWebhookEvent.html):

$signatureVerification = new VerifyWebhookSignature();
$signatureVerification->setWebhookId("YOUR-WEBHOOK-ID");
$signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
$signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
$signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']);
$signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
$signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);

$webhookEvent = new WebhookEvent();
$webhookEvent->fromJson($requestBody);
$signatureVerification->setWebhookEvent($webhookEvent);
$request = clone $signatureVerification;

try {
    /** @var \PayPal\Api\VerifyWebhookSignatureResponse $output */
    $output = $signatureVerification->post($apiContext);
} catch (Exception $ex) {
    print_r($ex->getMessage());
    exit(1);
}

Finally, we can read the webhook response and update our database etc depending on event type (renewal/cancellations):

$verificationStatus = $output->getVerificationStatus();
$responseArray = json_decode($request->toJSON(), true);

$event = $responseArray['webhook_event']['resource']['event_type'];

$outputArray = json_decode($output->toJSON(), true);

if ($verificationStatus == 'SUCCESS') {
  switch($event) {
    case 'BILLING.SUBSCRIPTION.CANCELLED':
    case 'BILLING.SUBSCRIPTION.SUSPENDED':
      // subscription canceled: agreement id = $responseArray['webhook_event']['resource']['id']
      break;
    case 'PAYMENT.SALE.COMPLETED':
      //subscription payment recieved: agreement id = $responseArray['webhook_event']['resource']['billing_agreement_id']
      break;
  }
}

The response varies depending on the event type hence the agreement id is stored under different keys within our switch statement above.

Back to Homepage

Find me on social media