LemonSqueezy vs PayPal: Merchant of Record vs Payment Processor
614 lines of LemonSqueezy integration compute zero sales tax, because LemonSqueezy is the merchant of record. What a PayPal-only checkout puts back on you.
LemonSqueezy is a merchant of record for digital products: it takes the sale, calculates and remits sales tax/VAT itself, and issues refunds from its own dashboard. PayPal is a payment processor: it moves money, and everything else — tax calculation, remittance, invoicing — stays your responsibility. They show up in the same "how do I get paid for a digital product" search, but they solve different halves of the problem, and this storefront's checkout code only exists because of that difference.
The distinction sounds academic until you look at what the checkout code actually has to do differently. A merchant of record's job is to stand between you and the buyer legally — the sale is theirs, the tax obligation is theirs, and your app's only remaining responsibility is to grant access once their webhook confirms payment. A payment processor's job is narrower: move the money, and leave the seller — you — holding everything downstream of that.
The short answer
Choose LemonSqueezy (or another merchant-of-record platform) if you're a small team selling digital downloads and don't want to register for sales tax in every jurisdiction that requires it. Choose PayPal, or add it as a payment option, if you already handle tax compliance yourself or you specifically need PayPal's brand recognition at checkout — LemonSqueezy itself lists PayPal as one of the payment methods it can accept, so the two aren't strictly mutually exclusive.
The axes that actually diverge
| LemonSqueezy | PayPal | |
|---|---|---|
| Role | Merchant of record | Payment processor |
| Sales tax / VAT | Calculated and remitted by LemonSqueezy | Your responsibility, jurisdiction by jurisdiction |
| Who the customer's statement shows | LemonSqueezy (or your store name via them) | You, directly |
| Refunds | Issued in the LemonSqueezy dashboard; a webhook notifies your app | Issued via PayPal's dashboard or API, same model |
| Subscription changes | Managed through your app + LemonSqueezy's customer portal | The customer must confirm plan changes via PayPal itself |
| Checkout integration | Hosted overlay (lemon.js) or hosted page | Redirect flow or the PayPal JS SDK's Buttons component |
| Webhook auth | HMAC-SHA256 signature over the raw body | Certificate-based signature verification via PayPal's API |
What it costs your package.json | Zero SDK — this storefront calls its API with fetch | The PayPal JS SDK, loaded client-side for Buttons |
What "merchant of record" actually removes from this codebase
The entire LemonSqueezy integration is 614 lines across six files:
$ wc -l src/lib/lemonsqueezy/*.ts src/lib/actions/checkout.ts \
src/app/api/webhooks/lemonsqueezy/route.ts
25 src/lib/lemonsqueezy/signature.ts
119 src/lib/lemonsqueezy/variants.ts
287 src/lib/lemonsqueezy/webhook.ts
43 src/lib/lemonsqueezy/webhookDb.ts
71 src/lib/actions/checkout.ts
69 src/app/api/webhooks/lemonsqueezy/route.ts
614 total
None of it computes tax. There is no rate table, no jurisdiction lookup, no VAT-number validation — because LemonSqueezy, as the merchant of record, is the legal seller and owns that problem entirely. A PayPal-only checkout wouldn't add those 614 lines back, but it would add the tax-compliance code this repo never had to write in the first place: at minimum, a tax-rate lookup by billing address and a way to remit what you collect.
The refund flow shows the MoR boundary clearly
src/lib/email/refundRequest.ts sends support one instruction, and it's a direct statement of who's actually authorized to move the money:
text:
`A buyer requested a refund.\n\n` +
`Order (internal): ${orderId}\n` +
`LemonSqueezy order: ${o.ls_order_id}\n` +
/* … */
`Refund in the LemonSqueezy dashboard; the order_refunded webhook will ` +
`auto-resolve the request and revoke access.`,
This codebase never calls a refund API — it can't, because LemonSqueezy is the seller of record and the refund is theirs to issue. Support clicks refund in LemonSqueezy's dashboard; the order_refunded webhook lands back on /api/webhooks/lemonsqueezy, and webhook.ts resolves the request and revokes entitlement automatically. A PayPal integration puts that authority back in your own hands — you'd call PayPal's refund API directly from your backend, using your own PayPal credentials, because you (not PayPal) are the merchant on record for the sale.
The identifier trap, and the webhook shape it produces
LemonSqueezy variants have two identifiers that are not interchangeable — a numeric id (what webhooks carry) and a UUID (what the hosted checkout URL accepts):
export function checkoutConfiguredForTier(tier: Tier): boolean {
return (
Boolean(variantIdForTier(tier)) && Boolean(checkoutUuidForTier(tier))
);
}
Swapping them fails silently: checkout opens fine, the buyer is charged, and the webhook's variant-to-tier lookup falls through to 202 ignored — money taken, nothing granted, no error anywhere. That's a LemonSqueezy-specific footgun, but the general shape — a webhook payload carrying identifiers your checkout-initiation code never touches — recurs with any provider. PayPal's webhooks carry their own resource ids (order id, subscription id) that are equally easy to conflate with whatever id your UI used to start the flow, and its signature verification is a different mechanism entirely: certificate-based, checked against PayPal's public certificate for the event, rather than the single HMAC secret this repo's verifySignature() compares against.
Two different shapes of checkout, and what each costs the CSP
BuyButton.tsx resolves a checkout URL server-side, then hands it to LemonSqueezy's own overlay script:
declare global {
interface Window {
LemonSqueezy?: { Url: { Open: (url: string) => void } };
}
}
// …
if (window.LemonSqueezy?.Url?.Open) {
e.preventDefault();
window.LemonSqueezy.Url.Open(checkoutUrl);
}
Clicking the button opens a hosted iframe on top of the page; if the overlay script hasn't loaded yet, the anchor's href still navigates to the same hosted checkout page as a fallback. That shape is why this site's CSP needs frame-src https://*.lemonsqueezy.com — the checkout itself is an iframe LemonSqueezy serves and controls, not markup this codebase renders.
A PayPal integration built on the JS SDK's Buttons component works the other way: PayPal's script renders the actual button and handles the click in your page's own DOM, with no iframe boundary for the button itself (payment capture may still route through PayPal-hosted UI depending on the flow). That shifts the CSP cost from frame-src to script-src — you're trusting a script to run inside your page rather than trusting an iframe to stay sandboxed. Neither shape is more secure in the abstract; they're different trust boundaries, and which one your CSP needs to reason about depends entirely on which SDK you loaded.
Mistakes and how they show up
| Symptom | Cause | Fix |
|---|---|---|
| Buyer is charged, gets no product | Numeric variant id and checkout UUID (or PayPal's checkout/webhook ids) were swapped | Refuse to sell a tier whose ids aren't both configured — see checkoutConfiguredForTier |
| Webhook returns 401 for every delivery | Signature computed over a re-serialized (parsed-then-stringified) body instead of the raw bytes | Read the raw request body first — req.text() before any JSON.parse |
| Checkout silently fails to open in production only | The provider's script/frame host is missing from the CSP | Add it explicitly — this repo names app.lemonsqueezy.com in script-src and *.lemonsqueezy.com in frame-src |
| Support can't explain a refund to a customer | The app tried to track refund state itself instead of trusting the provider's webhook | Let the provider's dashboard be the source of truth; resolve your side from its webhook, as order_refunded does here |
| Sales tax owed in a jurisdiction nobody registered for | Running a payment processor (PayPal-only) without separately handling MoR-style tax compliance | Either add a merchant-of-record layer, or budget for tax registration and remittance yourselves |
Frequently asked questions
Can I accept PayPal payments through LemonSqueezy? Yes — LemonSqueezy lists PayPal as one of the payment methods it can process, alongside cards. The comparison in this post is LemonSqueezy-as-merchant-of-record versus PayPal-as-your-own-processor, not "can these coexist."
Does PayPal handle sales tax like LemonSqueezy does? No. PayPal moves money; calculating and remitting sales tax or VAT on what you sell is your responsibility either way, which is the core reason a small digital-product seller often reaches for a merchant-of-record platform instead.
Why does this storefront check two identifiers before allowing a purchase?
Because LemonSqueezy's checkout URL and its webhook payload carry different identifiers for the same variant, and using the wrong one anywhere fails without an error — the buyer is charged and gets nothing. checkoutConfiguredForTier() refuses to render "Buy now" until both are set.
Is switching from LemonSqueezy to PayPal a small change? No — it's a change in legal role, not just a different SDK. You'd take on tax calculation and remittance, rebuild the webhook verification against PayPal's certificate-based model, and move refund authority from the provider's dashboard into your own backend.
What happens to a subscription if the customer needs to update their card? On LemonSqueezy, that's handled through the customer portal LemonSqueezy provides, linked from this app. On PayPal, subscription changes — including payment-method updates — generally need to be confirmed by the customer directly inside PayPal, which is a real UX divergence worth planning around if you expect self-serve billing management to live entirely on your own site.
Templates in this post
ASoc Brief is a product-designer résumé and portfolio site. ASoc Byte is an IT and startup engineering-studio site with AI services and social proof. ASoc Canvas is a no-code page-builder marketing site with a drag-and-drop editor pitch. Each is a marketing front for a product that would eventually need exactly this checkout decision — merchant of record, or a processor you handle tax for yourself.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
