Payments
SaaSyBase supports four payment providers through a unified abstraction. Switch between them with an environment variable — your business logic stays the same regardless of which provider processes the charges. The runtime supports richer multi-provider pricing data than the current admin plan modal exposes, so this page calls out both the underlying model and the current UI reality.
Note
How the Multi-Payment System Works
Like the auth system, payments use a provider pattern: an interface defines what every provider must implement, and a factory resolves the active provider from your environment at runtime. Your app code calls the abstraction — never a provider SDK directly.
PAYMENT_PROVIDER="stripe" # Options: "stripe", "paystack", "paddle", "razorpay"All providers share a common lifecycle: checkout → webhook → subscription activation. New transactions are routed to the active provider; existing transactions are handled by whichever provider originally processed them (stored in the paymentProvider field).
Warning
stripe) directly in your business logic. Always use PaymentProviderFactory.getProvider() from lib/payment/factory.| Provider | Preferred webhook endpoint | Extra alias routes | Notes |
|---|---|---|---|
| Stripe | /api/webhooks/payments | /api/webhooks/stripe and /api/stripe/webhook | Stripe is the only provider with both centralized and multiple explicit aliases. |
| Paystack | /api/webhooks/payments | /api/webhooks/paystack | Uses Paystack signature detection in the centralized router. |
| Paddle | /api/webhooks/payments | /api/webhooks/paddle | Centralized ingress is still the preferred production setup. |
| Razorpay | /api/webhooks/payments | No dedicated alias route is currently shipped | Razorpay webhook traffic should point at the centralized endpoint. |
Stripe
Tip
Official docs: Stripe docs and Stripe CLI docs.
Environment variables
PAYMENT_PROVIDER="stripe"
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..." # Supports comma-separated for rotation
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."Webhook setup
Endpoint: /api/webhooks/payments (preferred centralized endpoint) or /api/webhooks/stripe / /api/stripe/webhook.
Local testing:
stripe listen --forward-to localhost:3000/api/stripe/webhookCopy the printed secret into STRIPE_WEBHOOK_SECRET.
Recommended webhook events:
checkout.session.completedcheckout.session.async_payment_succeededinvoice.payment_succeeded/invoice.payment_failedinvoice.upcoming(renewal reminder emails)customer.subscription.created/updated/deletedcharge.refunded(optional)charge.dispute.*(optional)
Customer portal
Enable the Stripe Customer Portal in Stripe Dashboard → Settings → Billing → Customer Portal. Without this, the "Manage payment" button in the user dashboard returns an error.
Paystack
Tip
Official docs: Paystack docs.
Environment variables
PAYMENT_PROVIDER="paystack"
PAYSTACK_SECRET_KEY="sk_live_..."
PAYSTACK_WEBHOOK_SECRET="" # Optional — falls back to PAYSTACK_SECRET_KEY
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY="pk_live_..."Key details
- Webhook endpoints: Prefer
/api/webhooks/payments;/api/webhooks/paystackis the provider-specific alias. - Recommended webhook events:
charge.success,subscription.create,subscription.not_renew,subscription.disable,invoice.create,invoice.update,invoice.payment_failed, andrefund.processed. - Default currency: NGN. Supported: NGN, GHS, ZAR, KES, USD (USD requires merchant approval).
- Pricing model: Uses
plan_codeas the price ID. Subscriptions pass the plan code; one-time payments pass the raw amount. - Cancel at period end: Paystack has no native cancel-at-period-end. The app implements a workaround: it sets a flag in the database on cancel, then cancels in Paystack before the next charge fires on
invoice.created. - Manage payment: Uses the subscription's hosted
short_urlas a best-effort management page rather than a Stripe-style portal.
Paddle
Tip
Official docs: Paddle developer docs.
Environment variables
PAYMENT_PROVIDER="paddle"
PADDLE_API_KEY="pat_live_..."
PADDLE_WEBHOOK_SECRET="..."
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN="..."
PADDLE_ENV="sandbox" # or "production"
NEXT_PUBLIC_PADDLE_ENV="sandbox"
# Only needed when provider-side catalog sync is enabled
PAYMENT_AUTO_CREATE="true"
PADDLE_DEFAULT_TAX_CATEGORY="standard"The minimal Paddle setup is five values: PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, NEXT_PUBLIC_PADDLE_CLIENT_TOKEN, PADDLE_ENV, and NEXT_PUBLIC_PADDLE_ENV.
Everything else is advanced. Use PAYMENT_AUTO_CREATE plus PADDLE_DEFAULT_TAX_CATEGORY only if you want SaaSyBase to create Paddle catalog objects for seeded plans, and keep overrides like PADDLE_CURRENCY, PADDLE_WEBHOOK_TOLERANCE_SECONDS, or PADDLE_DEBUG_SUBSCRIPTION_UPDATES for exception cases.
Default payment link (required)
Paddle requires a Default payment link to generate checkout URLs. SaaSyBase provides a ready-made page at /paddle/pay:
- In Paddle Dashboard → Checkout → Settings, set the Default payment link to
https://YOUR_DOMAIN/paddle/pay. - The domain must be an approved website in Paddle.
- For local development, use an HTTPS tunnel (e.g. ngrok) and register the tunnel URL.
Tip
GET /api/admin/billing/paddle-config to diagnose missing configuration (Default payment link, prices, or credentials).For webhooks, point Paddle at /api/webhooks/payments or the alias route /api/webhooks/paddle.
Recommended webhook events: transaction.completed, subscription.created, subscription.updated, transaction.payment_failed, adjustment.created, and adjustment.updated.
Razorpay
Tip
Official docs: Razorpay docs.
Environment variables
PAYMENT_PROVIDER="razorpay"
RAZORPAY_KEY_ID=""
RAZORPAY_KEY_SECRET=""
RAZORPAY_WEBHOOK_SECRET=""
NEXT_PUBLIC_RAZORPAY_KEY_ID=""
RAZORPAY_CURRENCY="USD" # Optional (default: INR)Key details
- Webhook endpoint: Razorpay uses the centralized
/api/webhooks/paymentsroute. A dedicated Razorpay alias route is not currently shipped. - Recommended webhook events:
payment_link.paid,payment.captured,payment.failed,refund.processedorpayment.refunded, plussubscription.activated,subscription.updated,subscription.cancelled, andsubscription.halted. - Checkouts are redirect-based: one-time payments use Payment Links, subscriptions use the Subscriptions API.
- Daily subscription constraint: Razorpay requires
recurringIntervalCount >= 7for daily subscriptions. - Razorpay Offers: Set
RAZORPAY_ENABLE_OFFERS=trueand embed an offer ID in the coupon's description field (razorpayOfferId=offer_ABC123).
Provider Feature Matrix
This comparison is based on the shipped provider implementations and current provider metadata. The older table mixed roadmap items with live runtime behavior, which was especially misleading around checkout UX.
Stripe
The most complete integration in the repo, with native discounts, proration, customer portal, disputes, and trial periods.
Coupons
SupportedProvider-native coupons and promotion codes
Discount objects and promotion-code flows are handled directly by Stripe.
Subscription changes
SupportedInline plan changes are supported
Upgrades and downgrades can stay in the active subscription lifecycle.
Proration preview
SupportedPreview and bill prorations in provider
The provider can calculate proration before you finalize the plan change.
Manage payment
SupportedHosted customer portal
Customers can manage cards, invoices, and subscriptions in Stripe-hosted UI.
Checkout flow
SupportedEmbedded checkout elements / Checkout Sessions
Stripe has the broadest checkout coverage in the shipped integration today.
Implementation notes
- Refunds, invoices, receipts, disputes, and trial periods are all implemented in the provider layer.
Paystack
Strong fit for Africa-focused billing, with hosted subscription management and app-level fallbacks where the API is thinner.
Coupons
PartialIn-app discounts only
No native coupon API; discounts are applied in SaaSyBase.
Subscription changes
PartialCancel + recreate flow
The provider does not offer native inline plan switching.
Proration preview
Not shippedNot shipped
Proration is not supported natively.
Manage payment
PartialHosted subscription manage link
Uses the provider subscription management URL rather than a full portal.
Checkout flow
PartialInline popup / hosted checkout
The current docs now avoid calling this Stripe-style embedded elements.
Implementation notes
- Cancel-at-period-end is handled with an invoice-created workaround before the next renewal charge.
- Trial periods are not shipped in the Paystack provider implementation.
Paddle
Merchant-of-record billing with strong subscription management and proration support, centered on Paddle Billing checkout flows.
Coupons
SupportedProvider-native discounts and codes
Paddle discount objects can be used without relying on app-only fallbacks.
Subscription changes
SupportedProvider-backed subscription updates
Plan changes stay inside Paddle Billing instead of manual cancel-and-recreate flows.
Proration preview
SupportedProration preview and billing are implemented
The shipped provider layer includes proration-aware upgrade and billing support.
Manage payment
SupportedHosted customer portal
Customers are sent to Paddle-hosted subscription management screens.
Checkout flow
PartialHosted Paddle checkout
The shipped integration relies on the Paddle checkout flow and the default payment link at /paddle/pay.
Implementation notes
- Refunds are implemented. The provider capability list does not currently advertise native trial-period support.
Razorpay
Good subscription coverage for India-focused products, but the shipped checkout path is still redirect-first and some lifecycle tooling remains narrower than Stripe.
Coupons
PartialIn-app discounts only
Offer IDs can be wired in, but there is no Stripe-style native coupon flow.
Subscription changes
SupportedSubscription update support is implemented
The provider layer supports updating active subscriptions without recreating them.
Proration preview
PartialImplementation gap remains
The provider includes proration/update code paths, but proration preview currently throws not implemented.
Manage payment
PartialHosted subscription short_url
Management is exposed through the provider short URL rather than a full customer portal.
Checkout flow
PartialRedirect-first today
One-time payments use Payment Links and subscriptions use subscription short URLs in the shipped flow.
Implementation notes
- Native cancel-at-period-end is supported in the provider capability list.
- The provider source mentions embedded checkout as a goal, but the current repo still ships redirect-first flows.
Why the checkout row changed
If you are choosing a provider for the first time, also read Pricing & Coupons and Webhooks. Those pages explain the user-facing tradeoffs and the production webhook setup that the provider matrix alone does not cover.
Currency System
The app resolves the active currency using a priority chain. The first non-empty value wins:
| Priority | Source | Scope |
|---|---|---|
| 1 | Provider-specific env var (PADDLE_CURRENCY, PAYSTACK_CURRENCY, RAZORPAY_CURRENCY) | Per-provider override |
| 2 | Admin setting: DEFAULT_CURRENCY | DB-backed default, set in admin settings |
| 3 | PAYMENTS_CURRENCY env var | Environment fallback |
| 4 | Provider default | NGN for Paystack, INR for Razorpay, USD for Stripe/Paddle |
Multi-currency pricing
Plans support per-provider localized pricing via the PlanPrice model. This allows different prices in different currencies for each provider simultaneously — for example, $10 USD on Stripe and ₦15,000 NGN on Paystack for the same plan.
Note
PlanPrice CRUD management. Today it manages the base plan fields, recurring cadence, team/token metadata, and advanced external provider price ID overrides. If you need full localized price-row management, use sync tooling, Prisma Studio, seed data, or build additional admin UI.Plan Catalog Sync
Seeded plans automatically receive provider-generated price IDs during npx prisma db seedwhen catalog auto-create is enabled. You don't need to hand-copy price IDs from your payment dashboard.
PAYMENT_AUTO_CREATE="true"When enabled, saving a plan without a provider price ID auto-creates catalog objects on the configured provider. The manual price ID field in the admin plan form is an advanced override — leave it blank for the normal flow.
This is the part the shipped admin UI supports well today: core plan data plus provider catalog sync and imported external IDs, not a full multi-row localized pricing editor.
Centralized Webhook Endpoint
/api/webhooks/payments is the preferred single endpoint for all payment providers. It auto-detects the provider from the request signature header:
| Header | Provider |
|---|---|
| stripe-signature | Stripe |
| x-paystack-signature | Paystack |
| paddle-signature | Paddle |
| x-razorpay-signature | Razorpay |
Provider-specific routes also exist: /api/webhooks/stripe, /api/stripe/webhook, /api/webhooks/paystack, and /api/webhooks/paddle. Razorpay currently relies on the centralized route only. The centralized endpoint is recommended because it simplifies configuration — one URL covers all providers.
Note
STRIPE_WEBHOOK_SECRET="whsec_primary,whsec_rotating". The app tries each secret in order until one verifies.The dedicated Webhooks page now lists recommended events for Stripe, Paystack, Paddle, and Razorpay in one place, so you do not have to infer the non-Stripe event set from implementation details.
Invoices & Refund Receipts
SaaSyBase generates PDF invoices and refund receipts server-side using pdf-lib. These work for all providers regardless of their native invoicing support.
- Invoices include site branding, invoice number, bill-to details, plan info, coupon breakdown, and payment summary.
- Refund receipts include refund ID, original transaction reference, refunded vs. original amount, and applied coupons.
Coupon System
The app includes a full coupon engine with provider-aware discount handling:
- Discount types: Percent-off and amount-off.
- Duration control: Once, repeating (N months), or forever.
- Plan restrictions: Limit coupons to specific plans.
- Usage limits: Max redemptions and time-bound availability.
- Provider sync: Coupons auto-sync to providers that support native coupons (Stripe, Paddle). For others (Paystack, Razorpay), discounts are applied in-app.
| Page | Path | Description |
|---|---|---|
| Admin | /admin/coupons | Create, edit, activate/deactivate coupons |
| User | /dashboard/coupons | View redeemed coupons and pending redemptions |
Adding New Providers
Start with the public Adding Payment Providers guide, then use the deeper repository guide at docs/adding-payment-providers.mdwhen you are implementing the provider. In short: implement the PaymentProviderinterface, register it, add webhook routing/signature detection, wire any client-side requirements, and write tests.

