Best Payment Integration Libraries for Node.js in 2026
·PkgPulse Team
TL;DR
Stripe for full control; LemonSqueezy or Paddle for Merchant of Record (they handle global taxes). Stripe (~4M weekly downloads) is the gold standard — every feature exists, rock-solid API, but you're responsible for VAT/sales tax collection. LemonSqueezy (~100K) and Paddle (~50K) are Merchant of Record services — they handle tax compliance in 100+ countries. For SaaS, MoR services save months of compliance work.
Key Takeaways
- Stripe: ~4M weekly downloads — most features, best API, you handle tax compliance
- LemonSqueezy: ~100K downloads — Merchant of Record, developer-friendly, flat 5% + $0.50 fee
- Paddle: ~50K downloads — MoR, B2B-friendly, handles invoicing and compliance
- MoR vs Direct — LemonSqueezy/Paddle collect taxes on your behalf; Stripe you do it yourself
- Stripe Tax — Stripe now offers auto-tax calculation (adds ~0.5% per transaction)
Stripe (Full Control)
// Stripe — checkout session (hosted payment page)
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// app/api/checkout/route.ts
export async function POST(req: Request) {
const { priceId, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/dashboard?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
client_reference_id: userId,
customer_email: (await getUser(userId)).email,
metadata: { userId },
// Collect billing address for tax
billing_address_collection: 'required',
// Automatic tax calculation (requires Stripe Tax setup)
automatic_tax: { enabled: true },
});
return Response.json({ url: session.url });
}
// Stripe — webhooks (handle payment events)
// app/api/webhook/stripe/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await activateSubscription(session.client_reference_id!, session.subscription as string);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await updateSubscriptionStatus(subscription.id, subscription.status);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await cancelSubscription(subscription.id);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await notifyPaymentFailed(invoice.customer as string);
break;
}
}
return new Response('OK');
}
// Stripe — subscription management
async function getCustomerPortalUrl(customerId: string) {
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.APP_URL}/dashboard`,
});
return session.url;
}
// Proration — upgrade/downgrade mid-cycle
async function upgradeSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await stripe.subscriptions.update(subscriptionId, {
items: [{
id: subscription.items.data[0].id,
price: newPriceId,
}],
proration_behavior: 'create_prorations',
});
}
LemonSqueezy (Merchant of Record)
// LemonSqueezy — checkout URL generation
const response = await fetch('https://api.lemonsqueezy.com/v1/checkouts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json',
},
body: JSON.stringify({
data: {
type: 'checkouts',
attributes: {
checkout_data: {
custom: { userId: userId },
email: user.email,
name: user.name,
},
product_options: {
redirect_url: `${process.env.APP_URL}/dashboard?success=true`,
},
},
relationships: {
store: { data: { type: 'stores', id: process.env.LEMONSQUEEZY_STORE_ID } },
variant: { data: { type: 'variants', id: variantId } },
},
},
}),
});
const { data } = await response.json();
const checkoutUrl = data.attributes.url;
// Redirect user to checkoutUrl
// LemonSqueezy — webhook handler
// app/api/webhook/lemonsqueezy/route.ts
import crypto from 'crypto';
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get('x-signature')!;
// Verify signature
const hash = crypto
.createHmac('sha256', process.env.LEMONSQUEEZY_WEBHOOK_SECRET!)
.update(body)
.digest('hex');
if (hash !== signature) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
const eventName = event.meta.event_name;
const customData = event.meta.custom_data;
switch (eventName) {
case 'order_created':
await activateUser(customData.userId, event.data.id);
break;
case 'subscription_created':
await createSubscription(customData.userId, event.data);
break;
case 'subscription_cancelled':
await scheduleSubscriptionCancellation(event.data.id, event.data.attributes.ends_at);
break;
case 'subscription_payment_failed':
await notifyPaymentFailed(customData.userId);
break;
}
return new Response('OK');
}
Comparison: MoR vs Direct
| Aspect | Stripe | LemonSqueezy | Paddle |
|---|---|---|---|
| Tax handling | Manual (or +0.5% Stripe Tax) | Automatic (they collect) | Automatic (they collect) |
| VAT/GST compliance | You (complex) | They handle | They handle |
| Fee | 2.9% + $0.30 | 5% + $0.50 | 5% + $0.50 |
| Payout | Direct to bank | Weekly/monthly | Weekly/monthly |
| Refunds | Stripe handles | LemonSqueezy handles | Paddle handles |
| Best for | High-volume, enterprise | Indie SaaS | B2B SaaS |
| Checkout UI | Hosted or custom | Hosted | Hosted |
When to Choose
| Scenario | Pick |
|---|---|
| Enterprise with legal/tax team | Stripe |
| Indie SaaS, don't want tax headaches | LemonSqueezy |
| B2B sales with invoicing | Paddle |
| High volume (>$1M ARR) | Stripe (lower fees at scale) |
| Digital goods, one-time sales | LemonSqueezy |
| Need subscription management portal | All three (built-in) |
| EU VAT compliance headaches | LemonSqueezy or Paddle |
Compare payment library package health on PkgPulse.
See the live comparison
View stripe vs. lemonsqueezy on PkgPulse →