1

I read this question and this one. They both said (a year ago) that recurring payments via the REST API was in the works. On my client's website, customers need to be able to pay either

  1. in full (all at once — e.g., $1200 at check out)
  2. in installments ($1200 over 6 months at $200 per month)

It is crucial that his website be notified when a customer pays. I have currently set this up for option #1:

app.get("/cart/checkout/paypal", isLoggedIn, isVerified, function (req, res) {
    var user = req.user;
    var paymentDetails = {
        "intent": "sale",
        "payer": { "payment_method": "paypal"},
        "redirect_urls": {
            "return_url": "http://localhost:5000/cart/checkout/success",
            "cancel_url": "http://localhost:5000/cart"
        },
        "transactions": [
            { "amount": { "total": user.cart.finalPrice.toFixed(2), "currency": "USD"},
            "description": "You are being billed for " + user.cart.finalPrice.toFixed(2)}
        ]
    };
    paypal.payment.create(paymentDetails, function (err, payment) {
        if (err) console.log(err);
        else {
            if (payment.payer.payment_method === "paypal") {
                req.session.paymentId = payment.id;
                var redirectURL;
                for (var i = 0; i < payment.links.length; i++) {
                    var link = payment.links[i];
                    if (link.method === "REDIRECT") redirectURL = link.href;
                }
                res.redirect(redirectURL);
            }
        }
    })
})

Then, the "return_url" (/cart/checkout/success) grabs all the correct session info and my database processes it.

app.get("/cart/checkout/success", isLoggedIn, isVerified, function (req, res) {
    var user = req.user,
        paymentId = req.session.paymentId,
        payerId = req.param("PayerID"),
        details = { "payer_id": payerId };
...

Is there a similar setup for option #2 (recurring payments). If not, is there a way for PayPal to notify my server every time a user has paid an installment with the outstanding balance and amount paid/etc.?

Community
  • 1
  • 1
royhowie
  • 11,075
  • 14
  • 50
  • 67
  • https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECRecurringPayments/ says that you should call `GetTransactionDetails` to get recurring payment info. Since, you can also get the next scheduled billing date from `GetRecurringPaymentsProfileDetails`, then you could store the next scheduled billing date in your database and then call `GetTransactionProfile` after that time to check if the payment was made. – Seth Difley Jun 20 '14 at 01:11

2 Answers2

4

Yes, there is now a way to do subscriptions within the new REST API. See the documentation.

OK, first you need to set the Client ID and secret you get from PayPal. I have both a testing and live environment

All {xxx} are my private application variables

public function __construct()
{

    $this->sandbox = {sandbox};
    if($this->sandbox) 
    {
        $this->host = 'https://api.sandbox.paypal.com';
        $this->clientId = {clientIdSandbox};
        $this->clientSecret = {clientSecretSandbox};            
    }
    else 
    {
        $this->host = 'https://api.paypal.com';
        $this->clientId = {clientId};
        $this->clientSecret = {clientSecret};           
    }
    $this->get_access_token();
}

I then go and get the access token

private function get_access_token() 
{
    $curl = curl_init($this->host.'/v1/oauth2/token'); 
    curl_setopt($curl, CURLOPT_POST, true); 
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_USERPWD, $this->clientId . ":" . $this->clientSecret);
    curl_setopt($curl, CURLOPT_HEADER, false); 
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 
    curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials'); 
    $response = curl_exec( $curl );
    if (empty($response)) 
    {
        echo "NO RESPONSE for $url for function ".__FUNCTION__;
        print_r(curl_getinfo($curl));
        die(curl_error($curl));
        curl_close($curl); // close cURL handler
    } 
    else 
    {
        $info = curl_getinfo($curl);
        curl_close($curl); // close cURL handler
        if($info['http_code'] != 200 && $info['http_code'] != 201 ) 
        {
            echo "Received error: " . $info['http_code']. "\n";
            echo "Raw response:".$response."\n";
            die();
        }
    }
    $jsonResponse = json_decode( $response );
    $this->token = $jsonResponse->access_token;
    $this->expires = time()+$jsonResponse->expires_in;
}

This then stores the access data in the classes properties

You then need three more sections. Create the subscription template, then retrieve the agreement, then create the agreement for the client.

In this method I send over the data Name, Desc, Period, Interval and Price. However you can just fill in manually. This will create the subscription that you can now sell.

public function create_subscription($name, $desc, $period, $interval, $price)
{
    $data = array(
        'name' => $name,
        'description' => $desc,
        'type' => 'INFINITE',
        'payment_definitions' => array(
            0 => array (
                'name' => 'Payment Definition-1',
                'type' => 'REGULAR',
                'frequency' => $period,
                'frequency_interval' => $interval,
                'amount' => array(
                    'value' => $price,
                    'currency' => 'EUR',
                ),
                'cycles' => '0',
            ),
        ),
        'merchant_preferences' => array(
            'return_url'=>{return_url},
            'cancel_url'=>{cancel_url},
            'auto_bill_amount' => 'YES',
            'initial_fail_amount_action' => 'CONTINUE',
            'max_fail_attempts' => '0',
        ),
    );
    $data=json_encode($data);
    $url = $this->host.'/v1/payments/billing-plans';
    return $this->make_post_call($url, $data);  
}

From the above method you will get in return an id, use that for the method below to collect the data of the subscription and store it

public function retrieve_agreement($id)
{
    $url = $this->host.'/v1/payments/billing-agreements/'.$id;
    return $this->make_get_call($url);      
}

This method will allow you to allocate and agreement to a client. You will need the id of the aggreement with some data for you to be able add to the description.

public function create_agreement($subId, $data, $product)
{
    $paypalId = ($this->sandbox) ? $product->paypal_test_sub_id : $product->paypal_sub_id;
    $startDate = date('c', strtotime('+10 MINUTE'));
    $data = array (
        'name'=>'Subscription for subscription::'.$subId,
        'description'=>{company}.'  Subscription - ' . $data . ' - '.$product->name.' - '.$product->price .'€',
        'start_date'=>$startDate,
        'plan'=>array(
            'id'=>$paypalId,
        ),
        'payer'=>array(
            'payment_method'=>'paypal',
        ),
        'override_merchant_preferences'=>array(
            'return_url'=>{return_url}.$subId.'/',
            'cancel_url'=>{cancel_url}.$subId.'/',
        ),
    );  
    $data=json_encode($data);
    $url = $this->host.'/v1/payments/billing-agreements';
    $response = $this->make_post_call($url, $data);
    header("location:".$response['links'][0]['href']);      
    //return $response;
}

The return_url is the url that the end user will be sent to to complete the aggreement. I than use that to pass to the method below

public function execute_agreement($token)
{

    $data=json_encode('');
    $url = $this->host.'/v1/payments/billing-agreements/'.$token.'/agreement-execute';
    return $response = $this->make_post_call($url, $data);
}

You will then need to create a scheduled task to use the retrieve_agreement method and see if a subscription has been cancelled or not.

This is a brief explanation.

if you require more please let me know.

Get and Post

private function make_post_call($url, $postdata) 
{
    $curl = curl_init($url); 
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
                'Authorization: Bearer '.$this->token,
                'Accept: application/json',
                'Content-Type: application/json'
                ));

    curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata); 
    $response = curl_exec( $curl );
    if (empty($response)) 
    {
        echo "NO RESPONSE for $url for function ".__FUNCTION__;
        print_r(curl_getinfo($curl));
        die(curl_error($curl));
        curl_close($curl); // close cURL handler
    } 
    else 
    {
        $info = curl_getinfo($curl);
        curl_close($curl); // close cURL handler
        if($info['http_code'] != 200 && $info['http_code'] != 201 ) 
        {
            echo "Received error: " . $info['http_code']. "\n";
            echo "Raw response:".$response."\n";
            die();
        }
    }
    $jsonResponse = json_decode($response, TRUE);
    return $jsonResponse;
}

private function make_get_call($url) 
{
    $curl = curl_init($url); 
    curl_setopt($curl, CURLOPT_POST, false);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
                'Authorization: Bearer '.$this->token,
                'Accept: application/json',
                'Content-Type: application/json'
                ));
    $response = curl_exec( $curl );
    if (empty($response))
    {
        echo "NO RESPONSE for $url for function ".__FUNCTION__;
        print_r(curl_getinfo($curl));
        die(curl_error($curl));
        curl_close($curl); // close cURL handler
    } 
    else 
    {
        $info = curl_getinfo($curl);
        //echo "Time took: " . $info['total_time']*1000 . "ms\n";
        curl_close($curl); // close cURL handler
        if($info['http_code'] != 200 && $info['http_code'] != 201 ) 
        {
            echo "Received error: " . $info['http_code']. "\n";
            echo "Raw response:".$response."\n";
            die();
        }
    }
    $jsonResponse = json_decode($response, TRUE);
    return $jsonResponse;
}
Liam
  • 536
  • 8
  • 23
  • 1
    I indeed would like to know all the steps. The documentation by PayPal is really not clear. They mention 100 different calls you can make, but do not clearly say do A then B then C... super confusing. – Alexis Wilke Mar 26 '15 at 03:17
  • Hiya. I will expand my answer with examples in the next day or so. – Liam Mar 27 '15 at 08:12
  • Hiya All. Sorry i haven't replied back, got busy and then forgot. You can thank royhowie for editing my post and therefore reminded me. I can put my class on pastebin, or give some simple examples here – Liam Jun 16 '15 at 07:42
  • Ah, i see royhowie has already supplied some code. I can supply in PHP if required. – Liam Jun 16 '15 at 07:43
1

I would recommend staying away from the REST API for now. It's just not complete yet, and the Classic API gives you so much more flexibility.

I'd go with Express Checkout with Recurring Payments, and then you'll want to use Instant Payment Notification (IPN) to handle processing payments, canceled profiles, etc.

IPN notifications will actually be triggered for any transaction that ever hits your account, so you can automate the processing of payments, refunds, disputes, subscriptions, etc. You can update your database, send email notifications, or anything else you need to automate based on these transaction types.

IPN is one of the most valuable tools PayPal provides, yet it's also one of the most underutilized.

Drew Angell
  • 25,968
  • 5
  • 32
  • 51
  • do you consider @Liam 's answer still as non-recommended? i'm in need to implement a simple recurring payment without any `IPN`s and stuff. – ulkas Sep 28 '14 at 21:12
  • I think the support of the REST API is pretty good as it stands. Only the documentation is ultra complicated to follow. – Alexis Wilke Mar 26 '15 at 03:18
  • It has most of the basic things involved with payments, but if you've been using Express Checkout for a while and you know all of the additional features it provides you'll be missing them if you move to REST at this point. – Drew Angell Mar 26 '15 at 03:48
  • Hiya Andrew, I have never used the Classic, so I have no experience, but I have found that the REST is adequate for my needs – Liam Jun 16 '15 at 08:01