Introduction
The payment layer is one of the most expensive infrastructure decisions in a Node.js SaaS application. It looks simple at first: add a checkout button, listen for webhooks, update the user’s subscription status, and ship the product. In production, the decision is broader. You are choosing who owns card processing, subscription state, invoicing, sales tax, VAT, chargebacks, fraud review, customer billing support, revenue reporting, and sometimes the legal merchant relationship with the customer.
For Node.js teams, the best payment platform is not always the one with the lowest card processing percentage. A solo developer selling a $12/month tool worldwide has a different risk profile from a B2B SaaS company selling annual contracts, a marketplace splitting payments to creators, or an AI product with usage-based billing and enterprise procurement requirements.
This guide compares payment APIs and merchant-of-record platforms from a Node.js SaaS production perspective. It focuses on integration model, subscription support, webhook reliability, pricing factors, tax responsibility, and when each category makes sense.
Quick Recommendation
Use Stripe if you want the broadest developer API, strong Node.js support, custom checkout flows, marketplace payments, and long-term flexibility. Use Paddle or Lemon Squeezy if you are selling software or digital products globally and want the provider to act as merchant of record. Use Chargebee when your billing logic, revenue operations, usage-based pricing, dunning, and finance workflows are more complex than your checkout UI. Use Braintree when PayPal support and traditional payment gateway workflows are central to your business.
A simple rule works for most early-stage SaaS teams:
- If your main risk is “I need to launch payments quickly,” pick hosted checkout and webhooks.
- If your main risk is “I do not want to manage global sales tax,” pick a merchant-of-record platform.
- If your main risk is “our billing rules are complex,” evaluate a billing platform before you hard-code subscription state into your Node.js app.
Comparison Table
| Platform | Best for | Node.js fit | Billing model | Tax / compliance model | Key cost factors | Watch before choosing |
|---|---|---|---|---|---|---|
| Stripe | Custom SaaS payments, marketplaces, subscriptions, startups that want API control | Excellent | Processor + optional Billing, Tax, Invoicing, Connect | You usually own merchant and tax obligations unless using separate products or arrangements | Card fees, Billing volume, Tax, Invoicing, Connect, currency conversion, chargebacks | Add-on costs can matter as you add billing, tax, invoices, fraud, and platform payments |
| Paddle | SaaS and digital products that want merchant-of-record coverage | Good | Merchant of Record with billing and subscriptions | Paddle acts as MoR for digital product businesses in many markets | MoR platform fees, payout terms, feature availability, plan terms | Less control than direct processing; confirm supported countries, product type, and pricing |
| Lemon Squeezy | Small SaaS, digital products, software downloads, creators moving into SaaS | Good | Merchant of Record with checkout, subscriptions, license keys, analytics | Lemon Squeezy positions itself as MoR and handles sales tax/VAT collection and filing | Platform fees, payment method fees, international transactions, payout rules | Good simplicity, but confirm limits, API needs, and custom billing requirements |
| Chargebee | Mature B2B SaaS billing, usage-based pricing, revenue operations | Good via APIs | Subscription billing platform | Usually integrates with payment processors and tax tools rather than replacing all merchant obligations | Annual platform cost, billing volume, add-ons, revenue recognition, retention, CPQ | May be too heavy for early startups with simple monthly subscriptions |
| Braintree | PayPal-heavy checkout, cards + wallets, gateway-style integrations | Good | Payment gateway / processor tools | Merchant responsibility depends on PayPal/Braintree setup and region | Card fees, PayPal fees, chargebacks, gateway setup, supported methods | Subscription and SaaS billing workflows may require more custom application logic |
| Adyen | Larger companies, global enterprise payments, many local payment methods | Good via API, more enterprise-oriented | Global payments platform | Usually enterprise contract and merchant responsibility varies by setup | Contract terms, local payment methods, risk tooling, volumes | More complex sales and integration process than startup-oriented platforms |
What Makes Payment APIs Different from Normal SaaS Tools
Payment providers sit on a boundary between code, finance, compliance, and customer support. A database provider can go down and recover. A monitoring tool can miss an alert. A payment system can create tax exposure, failed renewals, double charges, incorrect invoices, bad accounting data, or account freezes.
For a Node.js app, the payment provider affects several layers:
- Frontend checkout determines conversion and PCI scope.
- Backend payment API creates customers, checkout sessions, subscriptions, invoices, usage records, and refunds.
- Webhooks update local entitlement state.
- Your database maps provider customer IDs to users, workspaces, plans, seats, and feature limits.
- Background jobs retry failed webhook processing.
- Admin tools help support teams investigate payment status.
- Finance tools reconcile invoices, taxes, refunds, and revenue.
Because of that, the best payment API is not just an SDK choice. It is a production architecture decision.
Stripe: Best Overall Payment API for Node.js Control
Stripe remains the default choice for many Node.js SaaS products because its API surface is broad and its official libraries cover server-side and frontend integrations. Stripe lists server-side SDKs including Node, and its pricing page describes standard pay-as-you-go pricing with no setup fees or monthly fees for standard payments in the US, plus custom options for larger businesses.
Setting Up Stripe in a Node.js App
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-06-15.basil',
typescript: true,
});
async function createCheckoutSession(customerId: string, priceId: string) {
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/billing`,
metadata: { workspace_id: 'ws_abc123' },
});
return session.url;
}
For a Node.js SaaS app, Stripe is strongest when you need custom logic. You can start with Stripe Checkout, move into Billing for subscriptions, use Customer Portal for self-service plan management, add Connect for marketplace payouts, add Tax for calculation, and use webhooks to sync subscription state into your own database.
Stripe is also the most flexible option when your product may evolve. A simple SaaS can become usage-based, team-based, marketplace-based, or enterprise-invoiced without completely replacing the payment provider.
The tradeoff is that flexibility can create complexity. Once you add Billing, Tax, Invoicing, Connect, Radar, currency conversion, and international payment methods, your real cost can differ from the first card processing rate you saw. Choose Stripe when your engineering team wants API control and your company is willing to own the surrounding business operations.
Paddle: Best Merchant-of-Record Option for SaaS Companies Selling Globally
Paddle is different from a direct processor because it positions itself as a merchant-of-record platform for digital product businesses. Paddle says it manages payments, tax, compliance, and billing across 300+ markets. Its Node.js SDK integrates Paddle Billing with server-side JavaScript and TypeScript apps, includes TypeScript definitions, iterator-based pagination, and webhook signature verification helpers.
Paddle SDK Integration Example
import { Paddle, EventName } from '@paddle/paddle-node-sdk';
const paddle = new Paddle(process.env.PADDLE_API_KEY!, {
environment: 'sandbox',
});
async function createPaddleTransaction(customerId: string, priceId: string) {
const transaction = await paddle.transactions.create({
items: [{ priceId, quantity: 1 }],
customerId,
customData: { workspace_id: 'ws_abc123' },
});
return transaction.id;
}
The merchant-of-record model matters because the provider becomes the seller of record in supported transactions. That can simplify global launches for SaaS products that sell to customers in many countries but do not want to directly register, calculate, file, and remit sales tax or VAT across regions.
From a Node.js architecture perspective, Paddle is attractive when you want the payment provider to own more of the commercial workflow. Your app still needs secure webhook processing, entitlement mapping, and customer support tooling, but the tax and compliance burden can be lower than a processor-first stack.
The tradeoff is control. Merchant-of-record platforms are not just payment APIs; they are commercial platforms with specific rules about product type, supported regions, payout terms, checkout behavior, refunds, and customer communications. Before publishing or implementing, confirm current fees, country support, payout timing, and whether your exact product category is supported.
Lemon Squeezy: Best Simple Merchant-of-Record Stack for Small SaaS and Digital Products
Lemon Squeezy is useful for small SaaS products, indie software, paid templates, digital downloads, license keys, and products that want a simpler checkout and billing stack. Its public material describes it as an all-in-one platform for SaaS, subscriptions, global tax compliance, fraud prevention, multi-currency support, failed payment recovery, and PayPal integration. Its pricing page says sales tax and VAT collection are included in its merchant-of-record model.
For Node.js developers, Lemon Squeezy offers a REST API and an official JavaScript SDK. The API uses predictable resource-oriented URLs, JSON:API responses, standard HTTP response codes, authentication, and verbs. The JavaScript SDK is written in TypeScript and is designed to make billing integration easier in JavaScript applications.
import { LemonsqueezyClient } from 'lemonsqueezy.ts';
const ls = new LemonsqueezyClient(process.env.LEMON_SQUEEZY_API_KEY!);
async function createCheckout(variantId: number, email: string) {
const checkout = await ls.createCheckout({
data: {
type: 'checkouts',
attributes: {
product_options: { redirect_url: `${process.env.APP_URL}/billing` },
checkout_data: { email },
},
relationships: {
store: { data: { type: 'stores', id: process.env.LS_STORE_ID! } },
variant: { data: { type: 'variants', id: variantId.toString() } },
},
},
});
return checkout.data.attributes.url;
}
Lemon Squeezy is strongest when the business model is straightforward: a few subscription tiers, software licenses, downloads, or digital products. You can often ship faster than building a fully custom payment stack. The main question is whether your product needs advanced billing rules, deep accounting integration, complex seat management, custom enterprise contracts, or usage-based metering. If yes, compare it carefully with Stripe Billing, Chargebee, or a more specialized billing platform.
Chargebee: Best for Complex Subscription Billing and Revenue Operations
Chargebee is not just a checkout API. It is a subscription billing and revenue platform aimed at teams with more complex billing operations. Its pricing page lists a Performance plan for teams with subscription, usage-based, and hybrid sales models, with annual pricing shown for up to a billing-volume threshold.
This matters for Node.js SaaS companies that have outgrown simple monthly plans. Examples include usage-based pricing, custom invoices, coupons, annual contracts, sales-assisted upgrades, dunning workflows, revenue recognition, multiple entities, enterprise approval flows, and finance team reporting.
A Node.js app can integrate Chargebee through APIs and webhooks while keeping application-specific entitlement logic in its own database. The question is whether you want billing to be a product engineering concern or a revenue operations platform concern. Early startups often do not need this level of system. Larger B2B SaaS teams may save engineering time by avoiding a custom billing engine. Use Chargebee when billing rules have become a product of their own.
Braintree and PayPal: Best When PayPal Checkout Is Central
Braintree is relevant when your customer base expects PayPal, cards, wallets, and a more traditional gateway model. Its Node.js server setup documentation covers generating client tokens, and its Node.js SDK package has been a long-running integration option.
import braintree from 'braintree';
const gateway = new braintree.BraintreeGateway({
environment: braintree.Environment.Sandbox,
merchantId: process.env.BRAINTREE_MERCHANT_ID!,
publicKey: process.env.BRAINTREE_PUBLIC_KEY!,
privateKey: process.env.BRAINTREE_PRIVATE_KEY!,
});
async function generateClientToken(customerId?: string) {
const response = await gateway.clientToken.generate({ customerId });
return response.clientToken;
}
For pure SaaS subscriptions, Braintree may require more custom backend logic than Stripe Checkout or a merchant-of-record platform. You need to model customers, plans, subscription state, webhook handling, retries, failed payments, and admin operations carefully.
But if PayPal acceptance is a strategic requirement, Braintree deserves evaluation. The main question is not whether Braintree can process payments. It can. The question is whether it gives your SaaS team the full subscription, billing, tax, and self-service account-management workflows you need without building too much yourself.
The Webhook Layer Is the Real Payment Integration
Many tutorials focus on creating a checkout session. In production, webhooks are more important. Your Node.js app must treat the payment provider as the source of truth for financial events, but your own database as the source of truth for application entitlements.
Production-Grade Webhook Handler for Stripe
import { buffer } from 'micro';
import Stripe from 'stripe';
import type { NextApiRequest, NextApiResponse } from 'next';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-06-15.basil',
});
export const config = { api: { bodyParser: false } };
const PROCESSED_EVENTS = new Set<string>();
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
const workspaceId = subscription.metadata.workspace_id;
const status = subscription.status;
const currentPeriodEnd = new Date(subscription.current_period_end * 1000);
const priceId = subscription.items.data[0]?.price.id;
// Map Stripe price ID to your internal plan
const plan = await mapPriceToPlan(priceId);
// Update workspace entitlement in your database
await db.workspace.update({
where: { id: workspaceId },
data: {
plan: plan.name,
subscriptionStatus: status,
currentPeriodEnd,
stripeSubscriptionId: subscription.id,
},
});
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const sig = req.headers['stripe-signature'] as string;
const rawBody = await buffer(req);
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return res.status(400).json({ error: 'Webhook signature verification failed' });
}
// Idempotency: skip already-processed events
if (PROCESSED_EVENTS.has(event.id)) {
return res.status(200).json({ received: true, deduplicated: true });
}
PROCESSED_EVENTS.add(event.id);
// Queue the event for async processing
const eventType = event.type;
const eventData = event.data.object as Record<string, unknown>;
await queue.add('payment-event', {
eventId: event.id,
eventType,
data: eventData,
receivedAt: Date.now(),
});
return res.status(200).json({ received: true });
}
Queue-Based Processing Worker
import { Worker } from 'bullmq';
const worker = new Worker('payment-event', async (job) => {
const { eventId, eventType, data } = job.data;
// Log for audit trail
await db.auditLog.create({
data: {
eventType,
eventId,
payload: JSON.stringify(data),
processedAt: new Date(),
},
});
switch (eventType) {
case 'customer.subscription.updated':
case 'customer.subscription.created':
await handleSubscriptionUpdate(data);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancellation(data);
break;
case 'invoice.payment_failed':
await handleFailedPayment(data);
break;
case 'invoice.payment_succeeded':
await handleSuccessfulPayment(data);
break;
default:
console.log(`Unhandled event type: ${eventType}`);
}
}, { connection: redisConnection });
A reliable webhook design should include:
- Idempotency. Every event must be safe to process more than once. Store provider event IDs and processing status.
- Signature verification. Never trust webhook payloads without verifying the provider signature.
- Queue-based processing. A webhook route should validate and enqueue work quickly, then a worker should update subscription state.
- State reconciliation. A missed webhook should not permanently corrupt access. Build an admin action or scheduled job that re-fetches customer and subscription state from the provider API.
- Audit logs. Store what changed: user, workspace, provider customer ID, subscription ID, old plan, new plan, event ID, and timestamp.
- Grace periods. Decide what happens when payment fails. Do you lock the workspace immediately, limit premium features, or allow a grace window?
Cost Factors Developers Usually Miss
The headline card fee is only one part of payment infrastructure cost. Before publishing a pricing comparison or choosing a provider, confirm these items:
| Cost Category | What to Check |
|---|---|
| Card processing | Domestic cards, international cards, card-not-present fees |
| Currency | Conversion rates, multi-currency settlement |
| Payment methods | Local payment method fees |
| Tax | Tax calculation, filing, registration requirements |
| Invoicing | Invoice generation, custom invoice fields |
| Subscription billing | Billing platform fees, metered billing costs |
| Chargebacks & fraud | Chargeback fees, fraud tool costs |
| Refunds | Whether processing fees are returned on refunds |
| Marketplace payouts | Connect/platform payout fees |
| Revenue analytics | Dashboard, export, reporting features |
| Failed payment recovery | Dunning, retry logic costs |
| Payout timing | Days to settlement, minimum payout thresholds |
| Platform fees | Monthly minimums, annual contracts, per-transaction add-ons |
| Enterprise features | Custom contracts, SLAs, dedicated support |
For example, Stripe’s pricing page lists standard domestic card pricing and separate pricing areas for other products such as Billing. Lemon Squeezy’s pricing page emphasizes included merchant-of-record tax collection and filing, but that does not mean every use case has identical economics. Chargebee’s public pricing shows an annual platform cost for the Performance plan.
These models are not directly comparable unless you compare the entire operating cost of accepting, renewing, refunding, taxing, and supporting payments. For a $10/month indie SaaS, a MoR platform fee can be worth it if it avoids tax administration and saves time. For a high-volume US-only SaaS with finance staff, a direct processor may be cheaper. For a B2B SaaS with custom contracts, a billing platform may reduce internal engineering cost even if the monthly software fee is higher.
Recommended Architecture for a Node.js SaaS Payment Stack
A production Node.js payment stack should be intentionally boring:
1. Use Hosted Checkout First
Avoid custom card collection unless conversion, branding, or advanced payment method requirements justify it. Hosted checkout offloads PCI compliance and reduces your attack surface.
2. Map Provider IDs to Your Database
-- Example schema for subscription mapping
CREATE TABLE workspace_billing (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id),
provider VARCHAR(20) NOT NULL, -- 'stripe', 'paddle', etc.
provider_customer_id VARCHAR(255) NOT NULL,
provider_subscription_id VARCHAR(255),
provider_price_id VARCHAR(255),
plan VARCHAR(50) NOT NULL,
subscription_status VARCHAR(20) NOT NULL,
current_period_end TIMESTAMPTZ,
cancel_at_period_end BOOLEAN DEFAULT false,
last_synced_at TIMESTAMPTZ DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
Store customer ID, subscription ID, price ID, product ID, current status, current period end, cancellation status, and last synced time.
3. Use Workspace-Level Billing
Even if your app starts with one user per account, map billing to workspace, team, or organization early. This avoids painful data migrations when you introduce team plans.
4. Process Webhooks Through a Queue
Do not put complex entitlement updates inside the HTTP webhook response path. The invoice.paid handler should not update 50 database rows while Stripe waits for a 200 response.
5. Use Feature Flags for Plan Enforcement
Payment state should update plan entitlements, and application features should check entitlements consistently. Avoid sprinkling if (user.plan === 'pro') across your codebase.
// Entitlement checking pattern
async function canAccessFeature(workspaceId: string, feature: string): Promise<boolean> {
const billing = await db.workspaceBilling.findUnique({ where: { workspaceId } });
if (!billing || billing.subscription_status !== 'active') return false;
const planFeatures = PLANS[billing.plan]?.features ?? [];
return planFeatures.includes(feature);
}
const PLANS: Record<string, { features: string[] }> = {
free: { features: ['basic_analytics', '1_project'] },
pro: { features: ['advanced_analytics', 'unlimited_projects', 'api_access'] },
enterprise: { features: ['advanced_analytics', 'unlimited_projects', 'api_access', 'sso', 'audit_logs'] },
};
6. Create Admin Tools
Support needs to see payment state, provider links, invoices, failed webhook events, refunds, and last reconciliation status. Build a simple admin panel or at minimum provide CLI scripts that can look up customer state:
async function lookupCustomerBilling(workspaceId: string) {
const billing = await db.workspaceBilling.findUnique({
where: { workspaceId },
include: { auditLog: { take: 20, orderBy: { createdAt: 'desc' } } },
});
if (!billing) return null;
// Reconcile with provider
const stripeSub = await stripe.subscriptions.retrieve(
billing.provider_subscription_id!
);
return {
local: billing,
provider: {
status: stripeSub.status,
currentPeriodEnd: new Date(stripeSub.current_period_end * 1000),
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
},
drift: billing.subscription_status !== stripeSub.status,
};
}
7. Add Alerts
Failed webhook verification, repeated event processing failures, and subscription sync drift should trigger alerts. Monitor these metrics from day one.
Which Platform Should You Choose?
- Choose Stripe if you want long-term flexibility and do not mind owning more operational details. It is usually the safest default for technical teams that want a programmable payment foundation.
- Choose Paddle if you are selling SaaS or digital products globally and want merchant-of-record coverage with a developer-friendly billing platform.
- Choose Lemon Squeezy if you want a simple merchant-of-record checkout for digital products, subscriptions, licenses, and smaller SaaS launches.
- Choose Chargebee if subscription management, usage-based billing, finance workflows, and revenue operations are already too complex for custom code.
- Choose Braintree if PayPal-first checkout or a gateway-style payment stack is more important than an all-in-one SaaS billing workflow.
For most new Node.js SaaS projects, the practical shortlist is Stripe Checkout, Paddle Billing, or Lemon Squeezy. If you later need advanced billing operations, evaluate Chargebee before turning your application code into a billing platform.
FAQ
Should a Node.js SaaS app start with Stripe or a merchant-of-record platform?
Start with Stripe when you want maximum API control and can handle tax, invoicing, and compliance separately. Use a merchant-of-record platform when global tax handling, billing support, and faster international launch matter more than low headline processing fees.
What payment stack is best for a small SaaS with subscriptions?
For most small SaaS products, a hosted checkout plus webhooks is safer than building a custom card form. Stripe Checkout, Paddle Billing, Lemon Squeezy, or a subscription platform can reduce PCI and operational complexity.
What should developers check before publishing payment pricing comparisons?
Confirm local card fees, international fees, currency conversion, tax products, chargeback fees, invoice fees, MoR fees, payout rules, and feature availability before publishing because pricing changes by country and business model.
Conclusion
The best payment API for a Node.js SaaS app depends on what you want to own. Stripe gives engineering teams broad control. Paddle and Lemon Squeezy reduce tax and compliance burden through the merchant-of-record model. Chargebee helps when billing operations become too complex for custom code. Braintree remains relevant when PayPal and gateway-style payment acceptance are central.
Do not choose only by the first transaction fee you see. Model the full system: checkout, subscriptions, tax, invoices, refunds, chargebacks, failed payment recovery, admin tools, webhooks, reconciliation, and customer support. A slightly higher platform fee can be cheaper than months of custom billing code. A more flexible API can be worth the operational responsibility if your business model needs it.
Main Reference Sources
- Stripe Pricing & Fees: https://stripe.com/pricing
- Stripe SDKs: https://docs.stripe.com/sdks
- Paddle homepage and Merchant of Record description: https://www.paddle.com/
- Paddle Node.js SDK documentation: https://developer.paddle.com/sdks/libraries/node/
- Lemon Squeezy pricing: https://www.lemonsqueezy.com/pricing
- Lemon Squeezy API reference: https://docs.lemonsqueezy.com/api
- Lemon Squeezy JavaScript SDK: https://github.com/lmsqueezy/lemonsqueezy.js
- Chargebee pricing: https://www.chargebee.com/pricing/
- Braintree Node.js SDK: https://github.com/braintree/braintree_node
- Braintree Node.js server docs: https://developer.paypal.com/braintree/docs/start/hello-server/node/