PayPal Chained Payments in Yii – Using the PayPal Adaptive Payments API

A quite common problem in various projects is integrating a payment functionality where the site acts as the middleman between the author of a paid content and the end user. Premium themes or video courses, for instance. The user pays the website, then the website pays the author, keeping some cut to itself of course. That behavior can be automated with PayPal, and with them, it’s called Adaptive Payments, or, to be more specific, Chained Payments. In the documentation, they’re illustrated like this:


In this tutorial, we’ll follow a very simple practice to stick this mechanism into a Yii project. It will be easily modifiable, and you’ll be able to use it for nearly any payment purposes. Ready for it? Let’s go!

PayPal Developers’

First of all, let’s familiarize ourselves with the environment that PayPal is ready to provide for us. You can find it at After you log in, go to Sandbox Accounts, and you will find something like this:

Снимок экрана 2014-08-23 в 16.34.12

Wonder what that is and how it works? Well, as PayPal documentation puts it,

The biggest change when going live is the fact that you’re moving from virtual test accounts in the Sandbox to live accounts owned by real people who hold balances containing real money.

Therefore, before actually going live, you will have to work with PayPal’s Sandbox. To get it live after you’ve finished setting everything up, you’ll need to follow this guide and get permission from PayPal. However, the sandbox is a very good aspect for developers, as it allows you to familiarize yourself with the flow thoroughly.

To get started, we can create a few sandbox accounts. For an example, you can create three:, and (these are probably already taken, and you will have to consider different ones). The user will pay the site, and the site will pay the author. Provide the “user” account with a certain balance; you will be using that account and its password to make test payments. Our site will power the API with API credentials provided for the “site” account. So after creating, expand that account, select Profile and open the API Credentials tab. Save these. You will use them later.

Adding the Adaptive Payments API to the project

I have compiled a ready-to-use version of PayPal Adaptive Payments SDK. Normally, you would have to build up the endpoints by yourself from the samples, but this archive has the endpoint files written by me included, so you can just throw it inside your project and call it from inside the Yii scope.

Get the archive here. Copy it to your project root.

The main point here is the two files, initiate.php and verify.php. They are built by me based on the samples provided in the SDK. They take parameters from pre-loaded variables, call the PayPal API, and store the result in another variable that is then accessible in the Yii scope. How do we add this to the scope? We simply include the file.

If you look at initiate.php, it starts with this:

if(!isset($receivers) or !isset($amounts) or !isset($orderUrl)) die("Payment can only be initiated from the inside flow");

and ends with this:

$ack = strtoupper($response->responseEnvelope->ack);
if($ack != "SUCCESS") {
$errors = '';
foreach($response->error as $error);
$errors .= ' '.$error->message.'';
throw new CHttpException (400, 'We have had the following error: '.$errors);
} else {
$payKey = $response->payKey;
$token = $response->payKey;
$payPalURL = PAYPAL_REDIRECT_URL . '_ap-payment&paykey=' . $token;

As you see, the payment information was taken from three variables: $receivers, $amounts and $orderUrl. Then followed the API call, and the result was stored in $payKey and $payPalURL. If you follow the URL, it will lead you to the payment page. However, how do we do this inside of our Yii project? As you can see, the starting variables are not set anywhere in initiate.php. We will just need a controller inside of Yii where we will set these variables, then include the initiate.php. Moreover, the initiate.php itself will terminate if these variables are not set, making it only possible to launch from the inner scope.

Also, for security reasons, check that you have an .htaccess file in the paypal folder with the following line:

deny from all

If so, then you can be safe and sure. Nobody will be able to access the paypal directory, nor call the initiate.php directly. The call will be performed from a controller, where we’ll be taking care of it. But before we get to that, there’s one more thing inside the folder that we need to take care of. Remember the API credentials we looked at when we created the sandbox account? Open up paypal/samples/Configuration.php and check out these lines:

$config = array(
// Fill this in
"acct1.UserName" => "",
"acct1.Password" => "",
"acct1.Signature" => "",
"acct1.AppId" => "APP-80W284485P519543T",

acct1.AppId seems already filled. This is an application key that is identical for every sandbox app, so leave that as it is. As for the username, password and the signature, get that from the credentials tab we talked about and save the configuration file.

The inner scope

Now, we will set up a table in the database to store the payments in. I suggest the following:

Mind that I’m basing this tutorial is built upon what we’ve built in this tutorial on user management. We will be enjoying authentication and access control from there, so feel free to check that out too, or scrape the user part. Mind that for the same reason, the tables here are prefixed with tbl_. If you’re not using the plugin that introduced that, don’t do so.

For the tutorial’s sake, we’ll be pretending that we’re selling online courses! Okay, we’ll have to set up a table for that, too…

Снимок экрана 2014-08-23 в 17.52.45

The paypal field here represents the PayPal address of the course author. It will also have to be a real address (speaking sandbox terms, one of those you’ve created earlier). PayPal won’t let you pay a non-existing user.

Let’s create a sample course through the database, then. Assume the “author” email that we’ve created before was

Снимок экрана 2014-08-23 в 17.58.54

Our sample course will cost 1 dollar.

Next, generate the models for these two tables using Gii. In the payments model, we will also set up a few relations:

public function relations() {
return array(
'user0' => array(self::BELONGS_TO, 'User', 'user'),
'course0' => array(self::BELONGS_TO, 'Courses', 'course'),

This will link the paying user to his respective entry in the User model (if you followed that tutorial and have one, of course), and the course that he purchased to its model.

Don’t forget to remove the non-essential attributes from required rules in both models.

Okay, it’s time to write the controller now. We’ll call it PaymentsController.


class PaymentsController extends Controller

// only allow access for authenticated users

public function filters() {
return array(

public function accessRules() {
return array(
'actions'=>array('purchase', 'verify'),
'actions'=>array('purchase', 'verify'),
public function actionPurchase($course)
$course = Courses::model()->findByPk($course);
if(!$course) throw new CHttpException(404, "You are trying to pay for a course that does not exist.");

$adminPaypal = "";

$authorPaypal = $course->paypal;

// create a payment and fill it up with some basic information
$payment = new Payments;
$payment->course = $course->id;

$payment->user = Yii::app()->getModule('user')->user()->id;

$payment->status = 'CREATED';

$payment->date = date("Y-m-d H:i:s");
if(!$payment->save()) throw new CHttpException(400, "Error creating payment. Please contact support."); // if this is occurring to you, look at the rules you have in your model

$receivers = array($adminPaypal, $authorPaypal);
$amounts = array($course->price, $course->price*0.8); // this way, the website's cut is 20%
$orderUrl = Yii::app()->createAbsoluteUrl('payments/verify', array('order'=>$payment->id));
include "paypal/initiate.php"; // include our endpoint

$payment->key = $payKey; // grab the key after it gets to this point
if(!$payment->save()) throw new CHttpException(400, "Error creating payment. Please contact support."); // resave the payment with the key

$this->redirect($payPalURL); // redirect the user to the payment page

public function actionVerify($order)
$payment = Payments::model()->findByPk($order);
if(!$payment) throw new CHttpException(404, "You are trying to verify a payment that has not been created.");

$payKey = $payment->key;
include "paypal/verify.php";
$payment->status = $status;
if(!$payment->save()) throw new CHttpException(400, "Error processing payment. Please contact support.");

if($status == 'COMPLETED')

// At this point, the user had successfully paid, and you can do whatever should come next.

Yii::app()->user->setFlash('paymentResult', "You've successfully purchased this course.");
else if($status == 'INCOMPLETE' or $status == 'ERROR' or $status == 'REVERSALERROR' or $status == 'PROCESSING' or $status == 'PENDING') Yii::app()->user->setFlash('paymentResult', "The payment has not been fully completed. Please contact our support to verify your purchase.");
else Yii::app()->user->setFlash('paymentResult', "The payment has not been completed.");

// the above sets a flash, the below redirects the user to the potential course page

$this->redirect(array('/courses/view', 'id' => $payment->course));


Then direct your browser to Success!

Снимок экрана 2014-08-23 в 18.20.39

After completing the payment, you will probably get an error, though, as the code above will redirect you to view the purchased course, and that functionality does not exist in our project yet. You can either create it, or replace the flashes and redirects with the simple die(“message”).

If we head back to our developer dashboard, we can also check how the payments went by viewing the emails that our potential users received from PayPal afterwards. This is another good way to understand how the whole thing works, as if it really did complete the payments:

Снимок экрана 2014-08-23 в 18.27.49

And this is just how it works. If you add some imagination, you can finish the courses functionality. For example, this is what you can do in the courses model:

public $purchased;
protected function afterFind()

if (Payments::model()->findByAttributes(array('course'=>$this->id, 'user'=>Yii::app()->getModule('user')->user()->id, 'status'=>'COMPLETED')))
$this->purchased = true;
else $this->purchased = false;


and deny access to if the course is not purchased. In the view, you can add the code to display the above flashes. It will actually look akin to this:

<?php if(Yii::app()->user->hasFlash('paymentResult')) echo TbHtml::alert(TbHtml::ALERT_COLOR_INFO, Yii::app()->user->getFlash('paymentResult')); ?>

You can make the paypal a profile field for users instead of a field in the courses model, and make them fill it in in their profile, simply recording the users that create the courses instead. In the user management tutorial, you will find information on how to create custom profile fields. You can also make a separate model for purchases and add entries to it at the success point.

The point of this approach, technically, is that you throw in the whole PayPal SDK, and create an endpoint that will take pre-defined variables and return different ones, then reference it from a Yii controller. This will give you complete freedom in manipulating the payment details, storing them in models and so on. You can modify this code for almost any kind of behavior.

Another thing you have to remember is that your application is not live on PayPal yet. If you want it to be, you’ll have to follow these instructions. Good luck with that!

And if anything’s unclear, feel free to ask in the comments!

Was this helpful? Share it!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>