Overview 📖
I dove into integrating PayPal subscriptions into a SaaS app using Rails on the backend and Next.js on the frontend. It took quite a bit of time, so I decided to document the process, tough decisions, tackling some tricky technical challenges, and building a smooth subscription workflow that really works.
Background 🌐
The challenge? I was building a subscription payment system for a SaaS startup that was part of the 2024 Cohort of AUT Venture’s IDEA2IMPACT program. Initially, I was all set to use Stripe because of its awesome developer support and ease of use. But when I found out Stripe wasn’t available in our target country, I switched gears and went with PayPal instead.
Why PayPal & The Initial Hurdles 🤔
Why PayPal?
✅ A solid alternative when Stripe isn’t an option
🔄 Supports recurring billing
💳 A well-established payment provider
The Challenges I Faced:
Unlike Stripe, PayPal’s subscription API meant I had to make a bunch of manual API calls.
There’s no slick built-in UI for managing subscriptions.
And on top of that, PayPal webhooks were a bit hit or miss in production.
Rolling Up My Sleeves with API Calls 🔧
Since PayPal doesn’t offer an out-of-the-box UI for subscriptions, I had to set everything up manually using cURL requests. Here’s a quick rundown of the process to setup subscriptions in your terminal:
Step 1: Generate an Access Token 🔑
encoded_credentials=$(echo -n "CLIENT_ID:CLIENT_SECRET" | base64 -w 0)
curl -v -X POST "https://api-m.sandbox.paypal.com/v1/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic $encoded_credentials" \
-d "grant_type=client_credentials"
Step 2: Create a Product 🛍️
curl -v -X POST "https://api-m.sandbox.paypal.com/v1/catalogs/products" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-d '{
"name": "My SaaS Product",
"description": "SaaS subscription product",
"type": "SERVICE",
"category": "SOFTWARE",
"home_url": "https://my-saas.com/"
}'
Step 3: Create a Billing Plan 💸
curl -v -X POST "https://api-m.sandbox.paypal.com/v1/billing/plans" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-d '{
"product_id": "PROD-12345",
"name": "Starter Plan",
"description": "Basic subscription plan",
"status": "ACTIVE",
"billing_cycles": [
{
"frequency": { "interval_unit": "MONTH", "interval_count": 1 },
"tenure_type": "REGULAR",
"sequence": 1,
"total_cycles": 0,
"pricing_scheme": { "fixed_price": { "value": "49.99", "currency_code": "USD" } }
}
],
"payment_preferences": { "auto_bill_outstanding": true, "payment_failure_threshold": 3 }
}'
Bringing It All Together in Rails & Next.js 🔄
Here’s how I built the subscription flow:
Frontend (Next.js): I rendered a PayPal button using the PayPal Button SDK. When a user clicked it and subscribed, PayPal returned a
subscriptionID
, which was then sent to my Rails backend.Backend (Rails): I saved the subscription details and set up scheduled jobs to periodically sync with PayPal for any updates.
Next.js Code for the PayPal Button 💻
const PaypalSubscriptionButton = ({ planId }) => {
useEffect(() => {
window.paypal.Buttons({
createSubscription: (data, actions) => actions.subscription.create({ plan_id: planId }),
onApprove: (data) => handleSubscriptionSuccess(data.subscriptionID),
}).render("#paypal-button");
}, []);
const handleSubscriptionSuccess = async (subscriptionID) => {
await fetch("/api/company_subscription", {
method: "POST",
body: JSON.stringify({
subscription: { paypal_subscription_id: subscriptionID, plan_id: planId }
}),
});
};
return <div id="paypal-button"></div>;
};
Rails API Endpoint to Save the Subscription 📝
class Api::CompanySubscriptionsController < ApplicationController
def create
subscription_params = params.require(:subscription).permit(:paypal_subscription_id, :plan_id)
subscription = Subscription.create!(subscription_params)
render json: { subscription: subscription }, status: :created
end
end
Keeping Track of Subscription Status ⏱️
Because PayPal webhooks were a bit unreliable, I set up a scheduled job that periodically fetches subscription updates:
class SyncPaypalSubscriptionDetailsJob < ApplicationJob
def perform(subscription_id)
subscription = Subscription.find_by(id: subscription_id)
return unless subscription
return if subscription.paypal_subscription_id.blank?
paypal_service = PaypalSubscriptionService.new
subscription_details = paypal_service.get_subscription_details(subscription.paypal_subscription_id)
subscription.update(
status: subscription_details["status"].downcase,
next_billing_date: Time.zone.parse(subscription_details.dig("billing_info", "next_billing_time"))
)
end
end
Final Takeaway 🎯
This project was a deep dive into integrating PayPal subscriptions in a SaaS environment. While PayPal’s API required more hands-on work compared to Stripe, I managed to build a robust system by using scheduled polling to handle subscription updates.
Starting with a lean MVP and iterating as needed turned out to be a winning strategy.
References: