Paypal Website Payments Pro (US) with Recurring Billing and ActiveMerchant
August 1, 2008 – 12:12 pm by Chris Cera
Printable Version
When we decided to use Paypal Website Payments Pro (US), we were slightly bummed that ActiveMerchant didn’t support recurring billing. We almost jumped to a different payment processor, but finally decided to roll up our sleeves and write the code ourselves. In the spirit of giving back to the open source community, we have posted the code below under the MIT License used by ActiveMerchant.
I started with the code from the PayflowGateway class, and modified it until it worked with Website Payments Pro. I also took some ideas from a PaypalExpress implementation written by Oleksandr Bondar.
Here is the code:
# The MIT License
#
# Copyright (c) 2008 Vuzit.com, Chris Cera, Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'rubygems'
require 'active_merchant'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class PaypalGateway < Gateway
# I invented the :suspend action, and this doesn't appear in payflow.rb
RECURRING_ACTIONS = Set.new([:add, :cancel, :inquiry, :suspend])
@@API_VERSION = '50.0' # not sure if this overrides the variable in PaypalCommonAPI
# Several options are available to customize the recurring profile:
#
# * <tt>profile_id</tt> - is only required for editing a recurring profile
# * <tt>starting_at</tt> - takes a Date, Time, or string in mmddyyyy format. The date must be in the future.
# * <tt>name</tt> - The name of the customer to be billed. If not specified, the name from the credit card is used.
# * <tt>periodicity</tt> - The frequency that the recurring payments will occur at. Can be one of
# :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily, :semimonthly, :quadweekly, :quarterly, :semiyearly
# * <tt>payments</tt> - The term, or number of payments that will be made
# * <tt>comment</tt> - A comment associated with the profile
def recurring(money, credit_card, options = {})
options[:name] = credit_card.name if options[:name].blank? &amp;&amp; credit_card
request = build_recurring_request(options[:profile_id] ? :modify : :add, money, options) do |xml|
add_credit_card(xml, credit_card, options[:billing_address], options) if credit_card
end
commit('CreateRecurringPaymentsProfile', request)
end
# cancels an existing recurring profile
def cancel_recurring(profile_id)
request = build_recurring_request(:cancel, 0, :profile_id => profile_id) {}
commit('ManageRecurringPaymentsProfileStatus', request)
end
# retrieves information about a recurring profile
def recurring_inquiry(profile_id, options = {})
request = build_recurring_request(:inquiry, nil, options.update( :profile_id => profile_id ))
commit('GetRecurringPaymentsProfileDetails', request)
end
# suspends a recurring profile
def suspend_recurring(profile_id)
request = build_recurring_request(:suspend, 0, :profile_id => profile_id) {}
commit('ManageRecurringPaymentsProfileStatus', request)
end
private
def build_recurring_request(action, money, options)
unless RECURRING_ACTIONS.include?(action)
raise StandardError, "Invalid Recurring Profile Action: #{action}"
end
xml = Builder::XmlMarkup.new :indent => 2
ns2 = 'n2:'
if [:add].include?(action)
xml.tag! 'CreateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'CreateRecurringPaymentsProfileRequest' do
xml.tag! 'Version', @@API_VERSION, 'xmlns' => EBAY_NAMESPACE
# NOTE: namespace prefix here is critical!
xml.tag! ns2 + 'CreateRecurringPaymentsProfileRequestDetails ', 'xmlns:n2' => EBAY_NAMESPACE do
# credit card and other information goes here
yield xml
xml.tag! ns2 + 'RecurringPaymentsProfileDetails' do
xml.tag! ns2 + 'BillingStartDate', options[:starting_at]
end
xml.tag! ns2 + 'ScheduleDetails' do
xml.tag! ns2 + 'Description', options[:comment]
unless options[:initial_payment].nil?
xml.tag! ns2 + 'TrialPeriod' do
xml.tag! ns2 + 'BillingPeriod', 'Month'
xml.tag! ns2 + 'BillingFrequency', 1
xml.tag! ns2 + 'TotalBillingCycles', 1
xml.tag! ns2 + 'Amount', amount(options[:initial_payment]), 'currencyID' => options[:currency] || currency(options[:initial_payment])
end
end
frequency, period = get_pay_period(options)
xml.tag! ns2 + 'PaymentPeriod' do
xml.tag! ns2 + 'BillingPeriod', period
xml.tag! ns2 + 'BillingFrequency', frequency.to_s
xml.tag! ns2 + 'TotalBillingCycles', options[:payments] unless options[:payments].nil? || options[:payments] == 0
xml.tag! ns2 + 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
end
xml.tag! ns2 + 'AutoBillOutstandingAmount', 'AddToNextBilling'
end
end
end
end
elsif [:cancel, :suspend].include?(action)
xml.tag! 'ManageRecurringPaymentsProfileStatusReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'ManageRecurringPaymentsProfileStatusRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! ns2 + 'Version', @@API_VERSION
xml.tag! ns2 + 'ManageRecurringPaymentsProfileStatusRequestDetails' do
xml.tag! 'ProfileID', options[:profile_id]
xml.tag! ns2 + 'Action', action == :cancel ? 'Cancel' : 'Suspend'
xml.tag! ns2 + 'Note', 'Canceling the action, no real comment here'
end
end
end
elsif [:inquiry].include?(action)
xml.tag! 'GetRecurringPaymentsProfileDetailsReq', 'xmlns' => PAYPAL_NAMESPACE do
xml.tag! 'GetRecurringPaymentsProfileDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE do
xml.tag! ns2 + 'Version', @@API_VERSION
xml.tag! 'ProfileID', options[:profile_id]
end
end
end
end
def get_pay_period(options)
requires! (options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily, :semimonthly, :quadweekly, :quarterly, :semiyearly])
case options[:periodicity]
when :weekly then [1, 'Week']
when :biweekly then [2, 'Week']
when :semimonthly then [1, 'SemiMonth']
when :quadweekly then [4, 'Week']
when :monthly then [1, 'Month']
when :quarterly then [3, 'Month']
when :semiyearly then [6, 'Month'] # broken! i think
when :yearly then [1, 'Year']
end
end
end
end
end
Examples
Here are some incomplete snippets that should get you started with this code. Please note that the :payments and :initial_payment options were not part of the PayflowGateway.
Start a recurring payment profile:
options = {
# :name => # if not spec'd, the name on card will be used
# :profile_id => 'I-SEVK234C8U1M', # triggers :modify on recurring
:email => 'joe.customer@vuzit.com',
:starting_at => '2008-05-09T00:00:00', # change this
:periodicity => :monthly,
:comment => 'CHANGEME',
:billing_address => billing_address,
:payments => 0,
:initial_payment => 2500
}
ActiveMerchant::Billing::PaypalGateway.pem_file = @@fd_key
ActiveMerchant::Billing::Base.mode = :test
gateway = ActiveMerchant::Billing::PaypalGateway.new(
:login => @@merchant_username,
:password => @@merchant_password
)
response = gateway.recurring(amount, card, options)
Retrieve information about an existing payment profile:
response = gateway.recurring_inquiry(options[:profile_id])
Suspend an existing payment profile. Please note that this was not included in the PayflowGateway class:
response = gateway.suspend_recurring(options[:profile_id])
Cancel an existing payment profile:
response = gateway.cancel_recurring(options[:profile_id])
I strongly recommend to anyone extending the Gateway classes to immediately override the parse() method so you can inspect the Paypal response in it’s entirety, and not the cleaned version from ActiveMerchant.
We hope you find this contribution useful. This is our way of saying thank you to the Ruby community that have helped us so greatly.

32 Responses to “Paypal Website Payments Pro (US) with Recurring Billing and ActiveMerchant”
where do you save this file? and how do you integrate it with the vendor>plugins>active_merchant>lib>active_merchant>billing>gateways>paypal.rb file?
i’ve got it working, but i just copied and pasted the new bits into the paypal.rb file, but im quite certain that this is not the right way to do this
By travis on Aug 31, 2008
Great! I’m glad it was useful, and thanks for reporting your success.
I believe this is the right way to do it since it isn’t part of the gem or plugin (yet).
By Chris Cera on Aug 31, 2008
Thanks for your work. I have it working but I have one issue.
How do I get the paypal profile_id when I setup the recurring payment
response = gateway.recurring(@amount, creditcard, options)
Is it in the response and if so how do I access it.
Thanks,
Brian
By Brian on Sep 5, 2008
To integrate without copying/pasting just save the file in the ‘lib’ folder as ‘paypal_pro_recurring.rb’ or something. Then, add “require ‘paypal_pro_recurring’ to the bottom of environment.rb.
Also, thanks for this code. Saved us some time!
By Bryan Powell on Sep 6, 2008
Brian: I was re-reading these comments and saw your question about profile_id. The code isn’t in front of me but I believe it can be accessed with request.params['profile_id']
By Bryan Powell on Sep 7, 2008
Brian,
You access this from the response. You can do something like this:
response = gateway.recurring @amount, @card, @merchant_options
if response.success?
puts response.params['profile_id']
end
By Chris Cera on Sep 7, 2008
Thanks Chris. That was exactly what I was looking for.
By Brian on Sep 9, 2008
Is this the same as the PayFlow Pro recurring that is included in ACtiveMerchant now? I’m trying to get that working and it’s “recurring_inquiry” is not working.
Should I replace it with your code?
By Andy on Nov 7, 2008
Hi Andy,
No, PayFlo is different, but it seems to be pretty popular so you should have a good shot at resolving your issue. I recommend talking to the people on the ActiveMerchant mailing list:
http://groups.google.com/group/activemerchant
recurring_inquiry() worked for me in WPP.
You could always debug the gem directly in the directory: activemerchant-1.3.2/lib/active_merchant/billing/gateways/
The files you should start with are gateways/payflow.rb and paypal/paypal_common_api.rb
Best of luck. -Chris
By Chris Cera on Nov 7, 2008
Chris, thanks for making this code available, it’s been a lifesaver for me.
By Dan Sweeney on Nov 9, 2008
Hi Chris,
Thanks for the response! Recurring_inquiry is also working for me, but with a profile whose payments are failing and it’s retrying, the documentation says it should return a “RETRYING CURRENT PAYMENT”, but it seems to be returning “ACTIVE”. Any ideas?
Thanks,
Andy
By Andy on Nov 9, 2008
One question: I’m having trouble modifying existing profiles. Including :profile_id in the options doesn’t seem to modify the profile, it still creates a new one for me.
Any suggestions? Thanks.
By Dan Sweeney on Nov 9, 2008
@Dan: You’re very welcome. I haven’t done much with modification of profiles yet, so I’m not sure what the issue is. I know that merely declaring the :profile_id automatically triggers it to get modified. There are some differences between the sandbox, and the production paypal site so if you’re using the sandbox it might be an issue specific to that.
@Andy: I’m sorry, but I don’t know the answer. I would override the PaypalGateway::parse() method and print the XML response directly. ActiveMerchant might be hiding something critical in the response from Paypal.
I hope that helps guys. We are thrilled that you find this useful.
By Chris Cera on Nov 10, 2008
Thanks for this! It saved me a lot of time.
There’s one thing that gave me an issue which I’m surprised to see hasn’t been brought up yet. I kept getting errors from PayPal about the start date. If your start date is a Time or Date, use this:
:starting_at => start_date.utc.strftime(”%Y-%m-%dT%H:%M:%SZ”)
By Joshua on Nov 19, 2008
It is true that adding the :profile_id in the options triggers the :modify action, but this is nowhere handled in this code. The message error that Andi probably gets is ‘Invalid Recurring Profile Action: modify’. Even if :modify would be added to the RECURRING_ACTION set, the request is never built for this action.
So, we either start to write the code for this, or, on an update, we just cancel the old profile and create a new one. All you need to do is update the profile_id that you probably hold in the database somewhere. Not nice, but works.
By Sebastian on Dec 9, 2008
Hi,
I tried the code but found the following error, is it that I am not able to place the code nicely, please help me.
Thanks in advance.
Pavan
index 9337 out of string
RAILS_ROOT: C:/new_workspace/spinact_trunk_new
Application Trace | Framework Trace | Full Trace
vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_nv/paypal_nv_common_api.rb:173:in `[]=’
vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_nv/paypal_nv_common_api.rb:173:in `post_data’
vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_nv/paypal_nv_common_api.rb:192:in `commit’
vendor/plugins/active_merchant/lib/active_merchant/billing/gateways/paypal_gateway.rb:51:in `recurring’
app/controllers/purchases_controller.rb:362:in `make_payment_recurring’
app/controllers/purchases_controller.rb:33:in `inline_recur’
-e:2:in `load’
By Pavan Agrawal on Jan 19, 2009
I have never seen this error before, but your stack trace has paths that I do not have. What version of ActiveMerchant are you using? I used ActiveMerchant 1.3.2 for this code.
I suggest you revert to the previous version if possible.
By Chris Cera on Jan 19, 2009
A very thanks for the reply,
I did the same as what you mentioned but got the following error.
NameError in PurchasesController#inline_recur
uninitialized constant ActiveMerchant::Billing::PaypalProRecurring::PAYPAL_NAMESPACE
RAILS_ROOT: C:/new_workspace/spinact_trunk_new
Application Trace | Framework Trace | Full Trace
vendor/rails/activesupport/lib/active_support/dependencies.rb:493:in `const_missing’
lib/paypal_pro_recurring.rb:82:in `build_recurring_request’
lib/paypal_pro_recurring.rb:46:in `recurring’
app/controllers/purchases_controller.rb:362:in `make_payment_recurring’
app/controllers/purchases_controller.rb:33:in `inline_recur’
-e:2:in `load’
-e:2
By Pavan Agrawal on Jan 20, 2009
addition to the point above, I have made a file in lib dir and using it, I feel that it need some include of dependent file.
By Pavan Agrawal on Jan 20, 2009
Got it resolved, the onlychange I did was
class PaypalGateway < Gateway
into
class PaypalGateway < PaypalGateway
and the class file I have putted in lib dir, it is working fine to me.
Thanks a lot bloggers.
By Pavan Agrawal on Jan 20, 2009
Great, thank you for reporting the success. It’s much appreciated. -Chris
By Chris Cera on Jan 20, 2009
Dear All,
I got configured the paypal pro at merchant and did all the above code, its nice and gr8 code, I must say.
I have a question, we all are doing recurring payments, and suppose the recurring end date is crossed, then what will happen to the application? will it get the notification from paypal, if so, How to configure that into our application.
Thanks in advance.
By Pavan Agrawal on Jan 23, 2009
Hi, Paypal has a notification system called IPN that can be configured to give you notifications but I’m not very familiar with the services that are available. It would be best for you to post this question on the Paypal Website Payments Pro (US) forum since this is more a Web Service issue than something specific to ActiveMerchant.
By Chris Cera on Jan 23, 2009
Ok, Thanks for the help, I’ll post the solution, if I’ll get any.
By Pavan Agrawal on Jan 27, 2009
Dear All,
In the above code, Can I mark the expiration time for the subscription?
This is very urgent, thanks in advance.
By Pavan Agrawal on Feb 12, 2009
Hi Pavan,
I just did a quick search through the code, and I don’t see anything that supports this. You might need to :cancel the recurring profile. Best of luck, -Chris
By Chris Cera on Feb 12, 2009
I’m not receiving any IPN messages from PayPal. Is it possible that this code doesn’t pass my notify_url on to PayPal?
And if that’s the case, then how did you handle IPN messages? They’re rather vital for subscriptions.
By mcv on Feb 23, 2009
mcv, This code doesn’t do anything with IPN to my knowledge. We do receive an email everytime an initial or recurring payment is received, but I don’t believe we setup anything special for this purpose. I hope that helps, -Chris
By Chris Cera on Feb 23, 2009
Thank you very much
cheers
Gg
By Geegee on Nov 9, 2009
Thanks a lot, Chris. This code got me running pretty quickly with regards to recurring payments in Website Payments Pro. There were a few caveats or not quite obvious moments (the code above says it can accept a Time or Date object but I had to feed it a properly-formatted string) but over all this was quite helpful.
By Nick Carter on Nov 20, 2009
Hi Chris,
Thanks for this. I’ve been looking for an ActiveMerchant/PayPal recurring payments solution for a while now. Have you considered forking ActiveMerchant on GitHub, adding your code, and doing a pull request? This could be a great addition to the ActiveMerchant master branch.
Thanks,
Chris Your
By Chris Your on Feb 17, 2010
Hi Chris,
Thanks for letting me know it’s useful to you. Was it hard to find this on Google?
Ideally, this would be part of ActiveMerchant. I’ve definitely considered everything you mentioned regarding adding it to the master, but I just don’t have time right now.
The license permits anybody to do it, so hopefully somebody will be able to pick up my slack here.
By Chris Cera on Feb 17, 2010