Skip to main content
ASoc
Comparison

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.

The ASoc Team8 min read

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

LemonSqueezyPayPal
RoleMerchant of recordPayment processor
Sales tax / VATCalculated and remitted by LemonSqueezyYour responsibility, jurisdiction by jurisdiction
Who the customer's statement showsLemonSqueezy (or your store name via them)You, directly
RefundsIssued in the LemonSqueezy dashboard; a webhook notifies your appIssued via PayPal's dashboard or API, same model
Subscription changesManaged through your app + LemonSqueezy's customer portalThe customer must confirm plan changes via PayPal itself
Checkout integrationHosted overlay (lemon.js) or hosted pageRedirect flow or the PayPal JS SDK's Buttons component
Webhook authHMAC-SHA256 signature over the raw bodyCertificate-based signature verification via PayPal's API
What it costs your package.jsonZero SDK — this storefront calls its API with fetchThe 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

SymptomCauseFix
Buyer is charged, gets no productNumeric variant id and checkout UUID (or PayPal's checkout/webhook ids) were swappedRefuse to sell a tier whose ids aren't both configured — see checkoutConfiguredForTier
Webhook returns 401 for every deliverySignature computed over a re-serialized (parsed-then-stringified) body instead of the raw bytesRead the raw request body first — req.text() before any JSON.parse
Checkout silently fails to open in production onlyThe provider's script/frame host is missing from the CSPAdd 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 customerThe app tried to track refund state itself instead of trusting the provider's webhookLet 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 forRunning a payment processor (PayPal-only) without separately handling MoR-style tax complianceEither 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.

Keep reading

Comparison9 min read

Lemon Squeezy vs Polar: 543 Lines, and Which of Them Survive

Both are merchants of record, so the tax question is settled. An inventory of the vendor-specific code in this storefront, and what a migration would cost.

Read more
Comparison10 min read

MDX vs a Headless CMS: Choosing by Who Writes the Posts

An editorial-workflow decision wearing an architecture decision's clothes. What MDX buys you in CI, the Turbopack plugin trap, and the four cases where a CMS simply wins.

Read more