LemonSqueezy vs Stripe for Digital Products: Who Is the Seller of Record
The fee gap is $0.82 on a $39 order. What it buys is a tax registration you no longer hold — and one silent failure that charges the buyer and grants nothing.
The choice is not the fee. Lemon Squeezy is a merchant of record: it is legally the seller, so it charges, collects and remits VAT and sales tax in the jurisdictions it is registered in, and it owns the chargeback. Stripe is a payment processor: you are the seller, and every tax registration is yours. The price difference is what that transfer costs.
This storefront sells digital downloads through Lemon Squeezy. Below is the arithmetic at our actual price points, the parts of the integration that only exist because of the merchant-of-record model, and the cases where we would still reach for Stripe. We have not built the same store twice, so treat the fee table as arithmetic and the integration notes as experience.
What "merchant of record" actually transfers
Reseller, not processor. The customer buys from Lemon Squeezy; Lemon Squeezy buys from you. Three consequences follow, and they are the entire decision:
| Merchant of record (Lemon Squeezy) | Processor (Stripe) | |
|---|---|---|
| Who the buyer contracts with | The platform | You |
| Whose name is on the statement | The platform's | Yours |
| Tax registration and filing | Platform's obligation | Yours, per jurisdiction |
| Invoice and VAT number on the receipt | Issued by the platform | Issued by you |
| Chargeback handling | Platform disputes it | You dispute it |
| Payout | Net revenue, on the platform's schedule | Gross minus fees, on yours |
| Control over checkout | Hosted, themed | Whatever you build |
The tax line is the one that decides it for most people selling software downloads. Digital goods are taxed where the buyer is, not where you are. Sell a $39 template to a buyer in Germany and there is German VAT to charge, collect, report and remit — and the threshold at which that obligation starts, for a non-EU seller supplying EU consumers, is the first sale. Repeat for the UK, Australia, Norway, and a growing list of US states with economic-nexus rules for digital products.
The fee difference, at real prices
Our tiers are $39, $129 and $249, one-time. Lemon Squeezy publishes 5% + 50¢ per transaction; Stripe publishes 2.9% + 30¢ for a standard US online card payment, plus 0.5% if you add Stripe Tax to calculate and file the tax you now owe.
| Order | Lemon Squeezy (5% + 50¢) | Stripe (2.9% + 30¢) | Stripe + Tax (0.5%) | Difference |
|---|---|---|---|---|
| $39 | $2.45 | $1.43 | $1.63 | $0.82 |
| $129 | $6.95 | $4.04 | $4.69 | $2.26 |
| $249 | $12.95 | $7.52 | $8.77 | $4.18 |
At a hundred orders a month across those tiers, the gap is roughly $200–250. That is the number to hold against the alternative, which is not zero: registrations in each jurisdiction, a filing calendar, an accountant who understands cross-border digital supply, and the risk of getting it wrong in a jurisdiction that back-dates.
Two adjustments make the comparison fairer, and both cut the same way. Stripe adds 1.5% for international cards and 1% for currency conversion — and a template store's traffic is international by default. Lemon Squeezy's 5% is inclusive of that. Meanwhile Stripe's fee is on money you actually keep, whereas a merchant of record's percentage is charged on the tax-inclusive total in jurisdictions where the tax is added on top.
What the integration looks like when the platform is the seller
The commerce code in this repo is smaller than a self-serve tax integration would be, and the reason is structural: we never see a card, never compute a rate, never issue an invoice. What we do own is the grant — turning "an order happened" into "this account owns these templates". That is one signed webhook and one transactional write.
The signature is computed over the raw request bytes, which forces the shape of the route handler:
// Node runtime — required for `node:crypto`.
export const runtime = "nodejs";
export async function POST(req: Request) {
const raw = await req.text(); // never req.json() first
const signature = req.headers.get("X-Signature");
// ...
}
Parse before you verify and you are verifying a re-serialization of the payload, not the payload. Key order, whitespace and number formatting are all free to differ, and the failure is intermittent rather than total — the worst kind.
The verification itself has one detail that generic HMAC snippets get wrong:
export function verifySignature(rawBody: string, header: string, secret: string) {
if (!header) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(header, "hex");
return a.length === b.length && b.length > 0 && timingSafeEqual(a, b);
}
timingSafeEqual throws on a length mismatch, and a malformed header must resolve to false, not throw — a thrown error in a webhook handler is a 500, and a 500 is a retry. Buffer.from(str, "hex") never throws either; it silently truncates invalid hex at the last valid byte pair, which is why the explicit length check is doing real work.
After that, three checks in order: the store id must match by exact string equality, the purchased variant must map to a tier we sell, and the write must be atomic. Order creation and entitlement creation happen in one SECURITY DEFINER Postgres function whose on conflict (ls_order_id) do nothing is the sole arbiter of "was this new?" — which makes a duplicate delivery a no-op instead of a double grant. The refund event runs the same way in reverse. Gated file downloads covers what happens after the grant, and our checkout walkthrough covers the purchase side; neither is repeated here.
The trap that costs a real sale
A Lemon Squeezy variant has two identifiers and they are not interchangeable:
- the numeric id (
1974967), which webhook payloads carry atfirst_order_item.variant_id; and - the UUID, which is what the hosted
/checkout/buy/<id>path accepts — the numeric id 404s there.
Put the UUID in the variable the webhook uses and the failure is silent in the worst direction: checkout works, the buyer is charged, every webhook falls through the tier lookup to a 202 ignored, and nothing is granted. No error is raised anywhere, because from the platform's side nothing went wrong.
Our fix is refusal rather than detection. A tier is sellable only when the UUID, the numeric id and the store subdomain are all configured; miss one and the buy button does not render. It is worth writing that guard on day one, because the alternative is finding out from a support email.
Where we would still pick Stripe
Not a hedge — these are real cases the merchant-of-record model serves badly:
- You already have the tax problem solved. An established entity with registrations and an accountant is paying twice for the same service.
- Physical goods or services. Merchant-of-record platforms are built around digital supply; shipping, fulfilment and inventory are somebody else's product.
- Usage-based or complex subscription billing. Metering, proration and mid-cycle plan changes are Stripe's home ground.
- You need the money sooner, or in a specific way. Payout schedules and destination charges are yours to configure with a processor and the platform's to decide with a reseller.
- Checkout is part of the product. Hosted checkout is a constraint. Sometimes that constraint is the whole point; sometimes it is unacceptable.
- Marketplace payouts to third parties. Splitting a payment between sellers is Connect-shaped, not reseller-shaped.
The honest summary: for one-time digital downloads sold to consumers worldwide by a small team, the merchant-of-record premium buys a compliance department. For US-only B2B, or anything with an invoice-and-purchase-order flow, it buys much less.
Mistakes and how they show up
| Mistake | Symptom | Fix |
|---|---|---|
req.json() before verifying | Signature fails intermittently, only in production | Read req.text() and verify the raw bytes |
| Letting a malformed signature throw | 500s, then endless webhook retries | Length-check both buffers; return false |
| Grant written outside a transaction | Order row exists, entitlement missing | One RPC for order + entitlement |
| No idempotency key | Duplicate delivery grants twice | on conflict (order_id) do nothing as the arbiter |
| Numeric id / UUID swapped | Buyer charged, nothing granted, no error | Require both before a tier is sellable |
| Assuming "no tax under a threshold" | Back-dated liability in a jurisdiction with no threshold | Read the rules per jurisdiction, or transfer them |
| Comparing 5% to 2.9% | Decision made on the wrong number | Add Tax, international-card and conversion fees first |
| Awaiting the confirmation email inline | Slow mail provider delays the webhook response | Schedule it after the response; never block the grant |
Frequently asked questions
Is a merchant of record just a payment processor with higher fees? No. It is a reseller. The legal chain is buyer → platform → you, which is what moves the tax registration, the invoice and the chargeback. A processor moves money on your behalf and leaves all three with you.
Does using a merchant of record mean I never think about tax? About the sales tax on those transactions, largely yes — it is the platform's obligation to charge, file and remit. Income tax on the payouts you receive remains entirely yours, and so does the paperwork for the entity earning it.
Can I switch later? Yes, and the switching cost is not the payments code — it is entitlements. If your grants key off platform order ids, a migration has to map old identifiers to new ones. Ours are keyed to a stable product slug plus an internal order row, which is a portability decision as much as a data-modelling one.
Which fee model wins at low prices? The fixed component dominates. At $39 the 50¢ versus 30¢ gap is a fifth of the total difference; at $9 it would be most of it. If you sell cheap digital items in volume, model the fixed fee first and the percentage second.
What about chargebacks? With a merchant of record the platform disputes them and absorbs the fee; the practical exposure is a revoked entitlement, which is why the refund path must be as atomic as the grant. With a processor you handle the dispute, pay the fee, and need evidence — which for a download means retaining delivery records.
Storefront templates to build this on
Every storefront below ships the pieces this decision touches — a catalog, a cart, a checkout flow and product pages ready to point at whichever payment platform you choose. The digital-goods store is the closest fit for the merchant-of-record case: instant delivery, no shipping, buyers everywhere.
