#668·open-saas

feat: Add PayPal as a payment processor option

Author: mohammadali2549Created May 4, 2026Updated May 13, 2026

Why PayPal

PayPal is the most widely recognized payment brand on the planet. It has 430 million active accounts across virtually every country where someone might fork this template and build a product. That recognition translates directly to checkout conversion: users who hesitate at an unfamiliar processor click "Pay with PayPal" without a second thought, because they already have an account and they already trust it.

The existing providers are excellent, but their reach is concentrated in the US and EU. A meaningful share of this template's 1.7k forks are being built by developers in India, Brazil, Southeast Asia, and Latin America, markets where PayPal is the default expectation. This will make the template actually usable for a large part of its audience.

Beyond reach, PayPal is also the lower-cost, direct-payout option for builders who don't want a Merchant of Record model. Fees are competitive, payouts go straight to your business account, and a significant number of developers in emerging markets already have a PayPal Business account from before Stripe was available to them. For that profile of builder, there is currently no viable option in Open SaaS. This fills that gap.


Technical feasibility

I've studied the existing PaymentProcessor interface and all three current implementations. PayPal maps to all 6 interface members via @paypal/[email protected] (official, TypeScript-first):

Interface member PayPal SDK call Notes
id "paypal" Added to the union type in paymentProcessor.ts
createCheckoutSession (one-time) OrdersController.createOrder({ intent: CheckoutPaymentIntent.CAPTURE, ... }) Approval URL lives in result.links.find(l => l.rel === "approve")?.href — HATEOAS pattern, same as the subscription path below
createCheckoutSession (subscription) SubscriptionsController.createBillingPlan() then SubscriptionsController.createSubscription({ planId }) Requires a billing plan to exist first; plan ID can be stored in the existing paymentProcessorPlans map. Approval URL also via result.links
fetchCustomerPortalUrl - PayPal has no hosted portal. The interface return type is Promise<string | null>, so this is fully type-safe. UI falls back to a deep-link to paypal.com/myaccount/autopay, documented in the provider guide
webhook CHECKOUT.ORDER.APPROVEDOrdersController.captureOrder({ id }) ; BILLING.SUBSCRIPTION.*SubscriptionsController.getSubscription({ id }) One-time payments require an explicit captureOrder call in the webhook — one extra step vs Stripe. Webhook signature verification uses PayPal's cert endpoint rather than HMAC; the SDK surfaces this cleanly
webhookMiddlewareConfigFn Raw body middleware Same pattern as Stripe and Polar
fetchTotalRevenue TransactionSearchController.searchTransactions({ startDate, endDate, fields: "transaction_info", balanceAffectingRecordsOnly: "Y", pageSize: 100, page }) Date range is capped at 31 days by the API — loop using totalPages from the response. Amounts are returned as decimal strings in transactionDetails[].transactionInfo.transactionAmount.value (no /100 conversion needed, unlike Stripe/Polar)

The three known implementation quirks are all containable:

  1. No customer portal — UI shows PayPal deep-link. Documented.
  2. Order capture in webhook — extra captureOrder call for one-time payments. Documented as PayPal-specific.
  3. Billing plan prerequisite for subscriptions — plan IDs seeded once at setup and stored alongside the existing paymentProcessorPlans config. No runtime overhead.

Scope of the PR

Strictly additive. Nothing changes for existing users of Stripe, Polar, or Lemon Squeezy.

New files:

  • template/app/src/payment/paypal/paypalClient.ts
  • template/app/src/payment/paypal/paymentProcessor.ts
  • template/app/src/payment/paypal/checkoutUtils.ts
  • template/app/src/payment/paypal/webhook.ts
  • opensaas-sh/blog/src/content/docs/guides/payment-integrations/paypal.mdx

Modified files:

  • template/app/src/payment/paymentProcessor.ts — add "paypal" to the id union and export the new processor
  • template/app/src/payment/env.ts — add PayPal env schema
  • template/app/.env.server.example — add PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID
  • opensaas-sh/blog/src/content/docs/guides/payment-integrations/index.mdx — add PayPal to provider list

My plan

I'd like to take this on. I'll follow the exact structure of the Polar implementation as the reference, mirror the doc page format of the existing provider guides, and include sandbox test instructions.