• tel: 0845 475 2487 (UK)

Ruby on Rails, PayPal Express Recurring Payments using Active Merchant

Developed by Colin

I recently needed to setup recurring payments through PayPal’s express checkout for a subscription based site I have writen using Ruby on Rails. There is already an excellent framework for interacting with most payment gateways, including PayPal, for Ruby called Active Merchant. Unfortunately recurring payments support in Active Merchant for PayPal Express Checkout is limited to a script pasted into their lighthous bug tracking system. The trouble is that this script only covers creating subscription profiles and also later getting details of that profile, but I needed to be able to suspend and cancel subscriptions profiles as well as make changes to the subscription from my site.

**** UPDATE: ActiveMerchant recently removed the functionality to use PayPal’s NVP API and so this code will no longer work with the latest ActiveMerchant. Raymond Law has kindly ported the code to use the SOAP API and you can find out more information and usage on his blog. ****

Active Merchant is very easy to extend so I have written a Ruby class that can be dropped into /vendor/plugins/active_merchant/billing/gateways/ within your Rails project (assuming you have AM installed as a plugin)

Below is the code: (I have also attached the .rb file: paypal_express_recurring_nv.rb)

# simple extension to ActiveMerchant for basic support of recurring payments with Express Checkout API
#
# See http://http://www.gotripod.com/2008/09/07/ruby-on-rails-paypal-express-recurring-payments-using-active-merchant/
# for details on getting started with this gateway
#
#
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class PaypalExpressRecurringNvGateway < Gateway
      include PaypalNvCommonAPI
 
      LIVE_REDIRECT_URL = 'https://www.paypal.com/cgibin/webscr?cmd=_customer-billing-agreement&token='
      TEST_REDIRECT_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_customer-billing-agreement&token='
 
      def redirect_url
        test? ? TEST_REDIRECT_URL : LIVE_REDIRECT_URL
      end
 
      def redirect_url_for(token)
          "#{redirect_url}#{token}"
      end
 
      def setup_agreement(description, options = {})
        requires!(options, :return_url, :cancel_return_url)
        commit 'SetCustomerBillingAgreement', build_setup_agreement(description, options)
      end
 
      def get_agreement(token)
        commit 'GetBillingAgreementCustomerDetails', build_get_agreement(token)
      end
 
      def create_profile(money, token, options = {})
        commit 'CreateRecurringPaymentsProfile', build_create_profile(money, token, options)
      end
 
      def get_profile_details(profile_id)
        commit 'GetRecurringPaymentsProfileDetails', build_get_profile_details(profile_id)
      end
 
      def manage_profile(profile_id, action, options = {})
        commit 'ManageRecurringPaymentsProfileStatus', build_manage_profile(profile_id, action, options)
      end
 
      def update_profile(profile_id, options = {})
        commit 'UpdateRecurringPaymentsProfile', build_update_profile(profile_id, options)
      end
 
      def bill_outstanding(profile_id, money, options = {})
        commit 'BillOutstandingAmount', build_bill_outstanding(profile_id, money, options)
      end
 
      private
 
      def build_setup_agreement(description, options)
        post = {}
        add_pair(post, :billingagreementdescription, description)
        add_pair(post, :returnurl, options[:return_url])
        add_pair(post, :cancelurl, options[:cancel_return_url])
        add_pair(post, :billingtype, "RecurringPayments")
        add_pair(post, :paymenttype, options[:payment_type]) if options[:payment_type]
        add_pair(post, :localecode, options[:locale]) if options[:locale]
        add_pair(post, :billingagreementcustom, options[:custom_code]) if options[:custom_code]
        post
      end
 
      def build_get_agreement(token)
        post = {}
        add_pair(post, :token, token)
        post
      end
 
      def build_create_profile(money, token, options)
        post = {}
        add_pair(post, :token, token)
        add_amount(post, money, options)
        add_pair(post, :subscribername, options[:subscriber_name]) if options[:subscriber_name]
        add_pair(post, :initamt, options[:initamt]) if options[:initamt]
        add_pair(post, :failedinitamtaction, options[:failedinitamtaction]) if  options[:failedinitamtaction]
        add_pair(post, :profilestartdate, Time.now.utc.iso8601)
        add_pair(post, :billingperiod, options[:billing_period] ? options[:billing_period] : "Month")
        add_pair(post, :billingfrequency, options[:billing_frequency] ? options[:billing_frequency] : 1)
        add_pair(post, :totalbillingcycles, options[:billing_cycles]) if [:billing_cycles]
        add_pair(post, :profilereference, options[:reference]) if options[:reference]
        add_pair(post, :autobillamt, options[:auto_bill_amt]) if options[:auto_bill_amt]
        add_pair(post, :maxfailedpayments, options[:max_failed_payments]) if options[:max_failed_payments]
        add_pair(post, :shippingamt, amount(options[:shipping_amount]), :allow_blank => false) if options[:shipping_amount]
        add_pair(post, :taxamt, amount(options[:tax_amount]), :allow_blank => false) if options[:tax_amount]
        add_shipping_address(post, options[:shipping_address]) if options[:shipping_address]
        post
      end
 
      def build_get_profile_details(profile_id)
        post = {}
        add_pair(post, :profileid, profile_id)
        post
      end
 
      def build_manage_profile(profile_id, action, options)
        post = {}
        add_pair(post, :profileid, profile_id)
        add_pair(post, :action, action)
        add_pair(post, :note, options[:note]) if options[:note]
        post
      end
 
      def build_update_profile(profile_id, options)
        post = {}
        add_pair(post, :profileid, profile_id)
        add_pair(post, :note, options[:note]) if options[:note]
        add_pair(post, :desc, options[:description]) if options[:description]
        add_pair(post, :subscribername, options[:subscriber_name]) if options[:subscriber_name]
        add_pair(post, :profilereference, options[:reference]) if options[:reference]
        add_pair(post, :additionalbillingcycles, options[:additional_billing_cycles]) if options[:additional_billing_cycles]
        add_amount(post, options[:money], options)
        add_pair(post, :shippingamt, amount(options[:shipping_amount]), :allow_blank => false) if options[:shipping_amount]
        add_pair(post, :autobillamt, options[:auto_bill_amt]) if options[:auto_bill_amt]
        add_pair(post, :maxfailedpayments, options[:max_failed_payments]) if options[:max_failed_payments]
        add_pair(post, :taxamt, amount(options[:tax_amount]), :allow_blank => false) if options[:tax_amount]
        add_shipping_address(post, options[:shipping_address]) if options[:shipping_address]
        post
      end
 
      def build_bill_outstanding(profile_id, money, options = {})
        post = {}
        add_pair(post, :profileid, profile_id)
        add_amount(post, money, options)
        add_pair(post, :note, options[:note]) if options[:note]
        post
      end
 
      def build_response(success, message, response, options = {})
        PaypalExpressNvResponse.new(success, message, response, options)
      end
 
    end
  end
end

With this class installed using Active Merchant to set up a subscription / recurring payment through PayPal Express Checkout is easy. Firstly setup your gateway object:

gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => 'PAYPALEMAIL', :password => 'PASSWORD', :signature => 'PAYPALAPISIGNATURE' )

Then make a request to PayPal to setup the recurring payment. At this stage you pass through a description (which is what is shown to the user when they are asked to authorise the subscription so make it descriptive) and you also need to provide URLs on your site, that PayPal should redirect the subscriber back to when they either complete the payment, or alternatively if they choose to cancel.

response = gw.setup_agreement("Subscription £25 per month", :cancel_return_url => "https://mysite.com/cancel", :return_url => "https://mysite.com/complete" )

The request above returns us a token in the response from paypal and at this point we need to redirect the user to PayPal to authorise this subscription. The user will see the description “Subscription £25 per month” as sent in the previous request. We need to redirect the subscriber to PayPal using the following line of ruby:

redirect_to gw.redirect_url_for(response.token)

Once the user has authorised the subscription they are returned to the :return_url we specified earlier, at which point we can create the actual subscription using the following:

response = gw.create_profile(2500, response.token, :currency => "GBP", :reference => "34")

Note: PayPal are really confusing having one API for the US and another for the UK but if you are using PayPal Express (which is free) independently of PayPal Website Payments Pro (Which you need to pay for) the US PayPal Express API works for all countries (apart from Germany I believe) and as you can see above I am passing in the UK currency. I am using the US API and I have a UK PayPal account. Also note that I have passed in a reference (I have an IPN URL setup in my PayPal account – Unfortunately you cannot pass an IPN URL with the request) to be sent in the IPN.

The previous step completes the set up of our Subscription. However if we need to later get information on the subscription or change it, we need to extract the Profile ID from the response as follows:

profile_id = response.params["profileid"]

With this profile_id we can then later use these additional methods that I have included, such as getting details of the subscription profile using:

gw.get_profile_details(profile_id)

Update the subscription using various options (i.e. changing subscription amount shown below):

gw.update_profile(profile_id, :money => 3000, :currency => "GBP")

Manage the subscription, for example cancel it as follows:

gw.manage_profile(profile_id, "Cancel", :note => "Your subscription has been cancelled by us")

And finally bill any outstanding subscription balance:

gw.bill_outstanding(profile_id, 2500, :currency=> "GBP", :note => "£20 Overdue Subscription")

Please note that as of yet this class is not part of Active Merchant, however it has been added to Active Merchants case #17 If you want to use this you will have to add it manually as above.

I recommend reading PayPals Express Checkout Integration Guide and the Name Value Pair API Developer Guide and Reference for more information on what variables can be passed in each request to PayPal.

Bookmark and Share

39 Comments

leon

September 9, 2008

/vendor/plugins/active_merchant/billing/gateways/
should be:
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/

Jon

September 9, 2008

Thanks, I missed that one!

evan

September 16, 2008

Jon Thanks for this great tutorial. Any chance I could discuss this in more detail with you?

Jon

September 16, 2008

Yeah sure, just drop me an email via the contact form.

Thx

grosser

September 17, 2008

what does :reference=>34 do ??

when i return and do
gw = ..Gateway.new
gw.create_profile(2500, params[:token], :currency => “EUR”, :reference => “34″)

the response looks like this:
— !ruby/object:ActiveMerchant::Billing::PaypalExpressNvResponse authorization: avs_result: message: code: street_match: postal_match: cvv_result: message: code: fraud_review: message: Security header is not valid params: timestamp: “2008-09-17T14:40:32Z” correlationid: 9b289d182ec1 l_severitycode0: Error build: “690663″ version: “50.0000″ l_longmessage0: Security header is not valid l_shortmessage0: Security error l_errorcode0: “10002″ ack: Failure success: false test: true

Jon

September 17, 2008

It looks like it does not like your API username, password or Signature. Make sure you double check these by logging into your PayPal account and looking under Profile -> API Access.

Regards,

Jon

grosser

September 17, 2008

can you post some controller code?
I think im doing something wrong…
The credentials should be valid since the initial request does work(sending the user to my store+returning)

def abo
gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => CFG[:paypal_login], :password => CFG[:paypal_password], :signature => CFG[:paypal_signature] )
response = gw.setup_agreement(“Test Subscription”, :cancel_return_url => root_url, :return_url => abo_return_orders_url )
redirect_to gw.redirect_url_for(response.token)
end

def abo_return
gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => CFG[:paypal_account], :password => CFG[:paypal_password], :signature => CFG[:paypal_signature] )
response = gw.create_profile(2500, params[:token], :currency => “EUR”, :reference => “34″)
flash[:error] = response.to_yaml
flash[:notice] = response.params['profileid']
end

Jon

September 17, 2008

This is very bizarre as I am using exactly the same code as you – the only difference is the currency I am using is GBP, are you testing against the sandbox or live?

grosser

September 17, 2008

you used response.token which failed so i used params[:token]

ill give it another try tomorrow, maybe only a strange bug…
(ill try $/GBP too…)

grosser

September 18, 2008

i had to add:
require File.join %w[active_merchant billing gateways paypal_nv paypal_nv_common_api] or else i would get uninitialized constant ActiveMerchant::Billing::PaypalExpressRecurringNvGateway::PaypalNvCommonAPI when running db:migrate very strange…

Leon

September 23, 2008

Hi,

In your post:
“if you are using PayPal Express (which is free) independently of PayPal Website Payments Pro (Which you need to pay for) the US PayPal Express API works for all countries (apart from Germany I believe) and as you can see above I am passing in the UK currency.”

What do you mean by “apart from Germany I believe”? Is that to say Germany currency is not supported?

Jon

September 23, 2008

Hi Leon,

I am not totally sure what the issue is, but PayPal stated the following:

“Yes – the API is the same for the UK and the US. There are only some
small changes if you plan to accept payments in Germany as they have
some different payment systems over there that require slightly
different treatment.:

I am not sure what these changes are but if you do find out I would be greatful if you could amend the code to work for developers with German PayPal accounts.

Thanks

Thomas

October 1, 2008

Hi!

First, great plugin, and great explanation! This is very helpful!

I do have one small problem, though. I have pasted my controller code below, but the issue is that when the user clicks the “Agree and Continue” button in paypal, I get redirected to another paypal page, which the browser claims to not be able to connect to….

def change_amt
# TODO: check if this user already has a donation amount, and if so, update instead of creating a new one.
amount = (params[:donate][:amount].to_f * 100).to_i
gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => PAYPAL_ACCOUNT, :password => PAYPAL_PASSWORD, :signature => PAYPAL_SIGNATURE )
response = gw.setup_agreement(“Subscription $#{params[:donate][:amount]} per month”, :cancel_return_url => “http://#{PAYPAL_RETURN_URL}/cancel”, :return_url => “https://#{PAYPAL_RETURN_URL}/complete” )

session[:amount] = amount
redirect_to gw.redirect_url_for(response.token)
end

def complete
gw = ActiveMerchant::Billing::PaypalExpressRecurringNvGateway.new( :login => PAYPAL_ACCOUNT, :password => PAYPAL_PASSWORD, :signature => PAYPAL_SIGNATURE )
response = gw.create_profile(session[:amount], params[:token], :currency => “USD”, :reference => current_user.id)
current_user.paypal_profile_id = response.params["profileid"]
current_user.monthly_donation = session[:amount]
current_user.billing_date = Date.today
current_user.save

session[:amount]= nil
session[:response_token] = nil
flass[:notice] = “Your monthly donation has been updated”
redirect_to current_user
end

fyi: I’m using the :test parameter, so everything is going to http://www.sandbox.paypal.com

Any help anyone can offer would be much appreciated!

Thanks,
-Thomas

David

October 16, 2008

Hi Jon – great stuff, exactly what I am looking for. Thanks for taking the time to write this down. In reading through the PayPal express checkout rules it says that a person’s payment profile can only be increased by 20% in a 180 day period. Is there a better way to handle subscriptions through ActiveMerchant and PayPal without incurring that limitation? Perhaps something other than Express Checkout?

Jon

October 16, 2008

Hi David, I think even with a PayPal website payments Pro account you would be limited to how much of an increase you can process. It depends what you are trying to do. If it is a discount trial period PayPal may let you do this or get the user to sign up to a new recurring agreement?

Alternatively it may be worth looking at some of the other payment gateways supported by Active Merchant.

David

October 20, 2008

Thanks Jon, I’m going to start looking at other gateways that are supported by ActiveMerchant.

David

November 6, 2008

OK Jon, total newbie Rails question for you on this. How are you maintaining the state of the response object between redirects to the PayPal site? Any help you can provide is greatly appreciated.

David

November 6, 2008

Sorry Jon – nevermind. I just reread Grosser’s comments above and see how he was doing it. That worked great for me.

Adam

November 6, 2008

Awesome, thanks for this, works a treat and bugged me for weeks how to do this.

Have you done much with IPN notifications, your extension and paypal? I’m just not sure how I can tell if a user’s subscription was cancelled by them via Paypal?

Is there something in get_profile_details where I can get the status etc of and status/history of payments made? I want to be able to dynamically create and update invoices / receipts for my users.

Thanks!

Jon

November 6, 2008

Hi Adam,

Unfortunately to limitations in PayPals API, you can only recieve IPN notifications for subscriptions if you have set it up in your PayPal account (Under Profile – Instant Notification) – Enter the IPN address and you will then get notifications for new subscription profiles, each payment and if they cancel.

Also note from my experience the Sandbox is quite flacky and every so often stops sending these notifications and you have to switch this setting off and then back on again to get it working again. Luckily the Live environment does not suffer from these problems!

Adam

November 8, 2008

Excellent thanks Jon, turns out you’ve answered another question! The reason I wanted to talk with the API was because I was not receiving IPN notifications and I thought there was another way to collect the data.

Makes developing my platform 10x harder as their simulator is a load of tosh.

Thanks again!

nerbie69

November 17, 2008

HI jon,
Thanks for the great ‘extension’ for this gateway. I too am watching ticket 17 to see if they can add it to the core functionality of Active Merchant.

I’ve taken the liberty to mash your article/extension with one that Cody Fauser did. You’ll see it includes the controller and views needed to get your extension working in an app.

It’s available here:http://nerbie69.blogspot.com/2008/11/recurring-payments-using-paypal-express.html.

Two questions though: Grosser on 09/17 mentioned that you had response.token, when it should be params{:token] in the gw.create_profile line. Are you going to change that, or are we both missing something?

Secondly, this may be basic, but you store the profile_id in the db, right? I mean it makes sense and all, but i guess we’d need a model to hold that table/column right? where do you put it? Cause in my examples, i don’t really store any of the details, do you? Would love to know your thoughts on this profile storing.

Jon

November 17, 2008

@nerbie69

You should be able to use response.token (I do in my code) the token parameter is exposed by one of the classes I extend (can’t remember which) but it is part of the ActiveMerchant framework which handles normal PayPal Express NVP responses and it basically maps response[:token] to response.token. I am not sure why you are having difficulties with it?

I recommend storing the profileid as you can then suspend, modify or cancel the subscription profile. I store mine in the user model however it will greatly depend on your app. I also store the ipns for each transaction which allows me to use the standard NVP api to automate refunds etc from the transaction id.

I hope that helps,

Jon

nerbie69

November 17, 2008

thanks jon.

i found out why the response.token bit wasn’t working. In my version of activemerchant we need to add:

require File.dirname(__FILE__) + ‘/paypal_nv/paypal_nv_common_api’
require File.dirname(__FILE__) + ‘/paypal_nv/paypal_express_nv_response’

to the top of your extension. This gives us access to the response methods inside.

And your line profile_id = response.params[:profileid] really doesn’t store it, does it. It is just an example, eh? that’s cool if it is, i just want to make sure the code as written isn’t supposed to work.

Jon

November 17, 2008

@nerbie69

My class is as is, I run ActiveMerchant as a plugin so I am not sure if this affects thinks. I also have “include ActiveMerchant::Billing” in my contoller.

You are correct, I am only showing an example but in the case of my own code I have the following:

if response.success?

@user.payment_profile_id = response.params["profileid"]
@user.save
end

I hope that clarifies things..

nerbie69

November 17, 2008

i don’t think my profileid is being passed.

if i do a params.inspect, i would assume that all of the params coming from paypal would be included in the inspect call.

here is what is coming back.
{“token”=>”RP-1YC815571E051481U”, “action”=>”confirm”, “controller”=>”subscriptions”}

it appears it’s only token. since this call is coming from paypal, i wonder what i can do to ensure the profile_id is also passed. thoughts?

Jon

November 17, 2008

You need to call response = gw.create_profile(2500, response.token, :currency => “GBP”, :reference => “34″) first. Then the profile id will be in your response object

nerbie69

November 17, 2008

crud. that is what is borking each time… hmm, hitting the code to see why the response.token is not working for me.

Thanks eh. You’ve been a great help, sorry for the questions. maybe they’ll help someone else?

nerbie69

November 17, 2008

i still get the nil.object type of error, when i use response.token, (nil.token),

however, by adding if response.success?; @profile_id = params['profileid']; end
actually returns the profile id. i’m excited. thanks again.

nerbie69

November 18, 2008

is it possible that response is a reserved word? I mean i changed to an instance variable of @cat = gateway.get_profile_details(@user.profile_id), and i then do a @cat.inspect in my view, and i see all the paypal info.

however if i change @cat to response (not an instance variable but just a variable) and then do a response.inspect in my view, i get the usual response debug information (around 100 lines on my browser).

Just thought i’d share. needless to say, i’m sticking with @cat.

David Parkinson

November 19, 2008

Jon:

Great tutorial! I must it’s a real bummer with the Express Checkout API not supporting IPN. What are you to do if you collect payments for multiple websites from one PayPal account?

I also verified CreateRecurringPaymentsProfile API doesn’t seem to have any spot for the NotifyURL.

Pavan Agrawal

November 26, 2008

Great Tutorial !!!!

Martin

November 26, 2008

Jon:
what does paypal send to the ipn (does it work with paypal express?) and how do I capture the info.

Vaibhav

December 17, 2008

Hello Jon
i am getting this error after doing all the above mentioned things

NoMethodError (undefined method `add_pair’ for #):
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_express_recurring_nv.rb:56:in `build_setup_agreement’
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_express_recurring_nv.rb:25:in `setup_agreement’
/app/controllers/users_controller.rb:146:in `create’

Can you help me over this please

Amitava

December 19, 2008

uninitialized constant ActiveMerchant::Billing::PaypalExpressRecurringNvGateway::PaypalNvCommonAPI

The above exception is thrown after placing ‘paypal_express_recurring_nv.rb’ into ‘/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/’.

I have installed the plugin from github and it only has ‘paypal_common_api.rb’ but not ‘paypal_nv_common_api.rb’. If I change the line ‘include PaypalNvCommonAPI’ to ‘include include PaypalCommonAPI’ then it shows the following error:

NoMethodError (undefined method `add_pair’ for #):
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_express_recurring_nv.rb:56:in `build_setup_agreement’
/vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_express_recurring_nv.rb:25:in `setup_agreement’

Jon

December 19, 2008

@Vaibhav, @Amitava, this class is dependent on the PayPalNvCommonAPI class being present. I will need to investigate where it has been moved to or whether it has been removed and why as I know the project owners are keen to integrate my class.

[...] a monthly or yearly subscription plan with PayPal. Therefore, I did some googling and found that Jon Baker has already extended ActiveMerchant to add this functionality using PayPal’s Name-Value Pair [...]

sam

December 10, 2009

Hi…
Great tutorial..
I have implemented both “EXPRESS” & “SIMPLE” Gateway. I have tried the solution that you have given. But I am having error like “This transaction is invalid. Please return to the recipient’s website to complete your transaction using their regular checkout flow.” on paypal site.

Can help me out???

Thanks..

smit

December 10, 2009

Thanks for the great tutorial….
But I am also having the same problem that sam is facing…

Waiting for your solution..

Thanks.

Leave a comment