Recurring Payments with Digital Goods for Express Checkout is the 8 word name PayPal chose for their most convenient subscription product. The verbose name is a good indicator of the API’s complexity.
It’s actually quite easy to integrate Digital Goods subscriptions. We send and receive a few HTTP requests and voilà, PayPal creates a subscription.
Unfortunately, the PayPal documentation is a labyrinth that never succinctly outlines the content and order of the HTTP requests required.
Nate, a PayPal staff member, helped map the labyrinth with this blog post. He didn’t cover subscriptions though, so this post will. You should read Nate’s post before continuing with this post.
3 Steps to Subscriptions
In a nutshell, the process for creating a subscription is like so:
- Tell PayPal we want to create a subscription;
- Confirm the subscription;
- Activate the subscription.
This process is the same as that outlined in Nate’s blog post. However, the API request strings differ, so this post focuses on the request strings’ content.
You can use all the API request strings provided in this post with the
PPHttpPost() method Nate provides in his example.
Step 1: Request a Token
Before we can have a user agree to pay for a subscription, we need a token from PayPal to represent the subscription.
To request a subscription token, we Nate’s
PPHttpPost() function with the
$my_api_str parameter set to:
$my_api_str = $cred_str . "&METHOD=SetExpressCheckout" . "&RETURNURL=http://example.com/return.php?return=paid" . "&CANCELURL=http://example.com/return.php?return=cancel" . "&BILLINGTYPE=RecurringPayments" . "&BILLINGAGREEMENTDESCRIPTION=" . urlencode("Hacker Monthly Subscription") . "&CURRENCYCODE=USD" . "&MAXAMT=100";
At first, I thought when requesting a token I’d need to tell PayPal the details of the subscription, like the price, duration or frequency – we don’t. We just tell PayPal we want to create a subscription. We give PayPal the details of our subscription only when we activate it.
The MAXAMT Parameter
This parameter indicates the average price per billing period for the subscription. By default, PayPal sets this to $25.
If your subscription is for more than $25 per period, you must include the
MAXAMT parameter with a value equal to or greater than your price per period.
Step 2: Use the token to start the subscription.
Once we have a token and the user has agreed to subscribe, PayPal suggests we have them confirm the charges.
Referring again to Nate’s post, we can use
return.php and get the subscription’s details from PayPal with the API string:
$my_api_str = $cred_str . "&METHOD=GetExpressCheckoutDetails&TOKEN=" . urldecode($token);
We use the
GetExpressCheckoutDetails method regardless of whether the request is for a subscription or one-off purchase.
Although suggested by PayPal, in my opinion, this step is redundant. Display the subscription price, period & frequency before directing the user to PayPal, then they are already aware of the details when they return to the site. In such a case, we can skip this step and go to Step 3.
Step 3: Activate the Subscription
Once a subscriber has confirmed the subscription, we post a request to PayPal to give them the details of the subscription and ask them to activate the subscription.
To do this, we call the
PPHttpPost() function with:
$my_api_str = $cred_str . "&METHOD=CreateRecurringPaymentsProfile" . "&TOKEN=" . $_GET['token'] // Subscription we are activating . "&AMT=" . urlencode( '10.00' ) . "&CURRENCYCODE=USD" . "&PROFILESTARTDATE=" . urlencode( date( 'Y-m-d\TH:i:s', time() + ( 24 * 60 * 60 ) ) ) . "&BILLINGPERIOD=Month" . "&BILLINGFREQUENCY=1" . "&DESC=" . urlencode( 'Time Magazine subscription' ) . "&TOTALBILLINGCYCLES=3" . "&INITAMT=".urlencode("80.00")
This string creates a 3 month subscription for $10 per month, starting from tomorrow, with a $80 sign-up fee. You can customise the NVP parameters in this request to set the price, frequency, duration, sign-up fee and trial period of the subscription.
Never Mind the Bollocks, Here’s the PHP Library
If you want to bypass the PayPal labyrinth altogether, you’re in luck. I’ve released a PayPal Digital Goods PHP Class that is friendly to humans. It’s a work in progress, but it strives to make it simpler to interact with the PayPal API by:
- Human friendly variable names: To reduce request size, PayPal’s API uses shortened parameter names. As we create instances of the class server-side, it can afford to use longer, more human friendly names. For example,
initial_amountrefers to PayPal’s
- Abstracting PayPalisms: PayPal loves verbiage, I don’t. The class attempts to simplify some of PayPal terms to more colloquial terms. For example, the
get_subscription_details()function performs PayPal’s
- Not repeating code: we only need to create one instance of the class for each subscription in our application. The credential & NVP API strings for every request are then automatically built using simple function calls.
Check out the class on GitHub for more information.
Thank you. Thank you. Thank you.
Great to hear it helped Thomas. 🙂
So when does the WooCommerce extension come out? I know there’s a lot of people who would love to sell subscriptions, and implementing this as an easy-to-use WooCommerce plugin would probably be super popular. I’d buy it.
As WooCommerce doesn’t have a membership/subscriptions product type, I didn’t add subscriptions into the Digital Goods PayPal payment gateway. I considered it, but it felt like putting a hat on a cat – makes for rhyme not reason.
So once some kind of subscription option is added (or somebody builds an extension), perhaps Digital Goods will just be upgraded to work with that feature? Or you assume they’ll build it in to the Extension so it won’t be needed later?
Yes exactly, that’s the plan.
I really appreciate this and your examples on github. I do have a question though. I am hoping to also find a way to deal with the cancellations of subscriptions. I could not find anything pertaining to this in your examples and was hoping you may have something I am missing that I could use.
If not, could you suggest an ideal approach to dealing with IPN notifications on digital good subscription cancellations?
Hi Josh, a very good question. There is nothing built into the library to handle cancellations.
The ideal approach is to set an IPN URL as part of the application (rather than manually in the PayPal Admin dashboard).
I believe this can be done by setting the value of
PAYMENTREQUEST_n_NOTIFYURLparameter in the
DoExpressCheckoutPaymentoperation. PayPal has some (slightly out-of-date) documentation on this process here (which refers to
PAYMENTREQUEST_n_NOTIFYURLparameter by its deprecated handle
You can find the up-to-date documentation on the
If you do extend the library to add such functionality, please make a pull request on Github so I can merge it into the core library to share with others.
Sounds good. I will look into doing this but I imagine it will be considerably more mundane than your code, considering I am just beginning with php and paypal integration.
I have another question as well. I notice you can do purchases, and subscriptions, can your classes be used in conjunction to have one button that provides a digital purchase as well as a digital subscription at once?
Thanks again Brent.
I’m not sure if PayPal support having a purchase and subscription in the one checkout process, but you can always test it to find out!
Also, I do take on some freelance work. My rates are usually too high for bootstrapped projects, but if you’ve got a company backing this and don’t mind sharing the code I write in the public library shoot me an email.
Joshua, just a heads up. I’ve since learned it is possible to have both a purchase and subscription in the one checkout process. I need to refactor my library to make this possible for a product I’m working on, so the public library should have this functionality (along with clear documentation) in a couple of weeks. 🙂
I am glad to hear about the subscription and purchase in one. I am also struggling with the Custom field currently. When I set the field it does not show on any IPN reports. Doing it with a button manually includes it, however running it through your class leaves the field empty.
Found the problem.
if( ! empty( $this->custom ) )
$api_request .= ‘&PAYMENTREQUEST_0_CUSTOM=’ . urlencode( $this->custom );
if( ! empty( $this->purchase->custom ) )
$api_request .= ‘&PAYMENTREQUEST_0_CUSTOM=’ . urlencode( $this->purchase->custom );
Thanks Joshua, updated.