Lemon Squeezy as a merchant of record
Lemon Squeezy sells your digital products in its own name, and it, not you, answers to tax authorities for VAT and sales tax. You pay 5 percent plus 50 cents per transaction for that. As of 22 August 2026 the platform still accepts new sellers, but it belongs to Stripe and is building its own successor under a different name in parallel.
What a merchant of record does
A merchant of record is the company that is formally the seller toward your customer. Its name appears on the receipt and on the card statement, it issues the sales document, it charges tax based on where the buyer lives, it files the returns and pays the tax to the right authority. You sell the product to that company and it resells it onward. Your only tax relationship is with them, not with a thousand customers across forty jurisdictions.
That distinction decides whether an article about Lemon Squeezy makes sense at all. A 5 percent fee sounds expensive next to 2.9 percent at a plain payment processor, until you price out VAT OSS registration, tracking nexus thresholds across US states, monthly filings, and the accountant who handles all of it. For a one-person company selling a tool at 29 dollars a month, the difference in percentages is cheaper than the difference in hours.
Scope is limited to digital products: SaaS, downloads, subscriptions, memberships, courses. You cannot sell physical goods there, and the list of prohibited products in the documentation is longer than you would expect.
The platform's status after the Stripe acquisition
Stripe bought Lemon Squeezy on 26 July 2024. That is not rumour but a post on the platform's own blog. Since then the product's status has shifted enough that it has to be stated plainly before you decide anything.
On 28 January 2026 JR Farr, the co-founder who now signs off as "Co-Founder, Lemon Squeezy (Stripe)", published a post summarising that period. He admits three things in it. First, the team worked mainly on Stripe Managed Payments, the merchant of record solution being built inside Stripe. Second, that cost the Lemon Squeezy community slower support responses and less frequent product updates. Third, the goal is "to provide Lemon Squeezy users an easy way to migrate to Stripe Managed Payments".
No shutdown date appears anywhere. Signup works, pricing is published, documentation is being updated, and the marketing site invites you to open an account. At the same time Stripe Managed Payments supports merchants in more than 35 countries according to that post, while Stripe's documentation talks about tax compliance in more than 80 countries. Those are two different numbers describing two different scopes and should not be conflated.
There is one more detail you will not find in a press release. The footer of lemonsqueezy.com today reads "Sold through Link, LLC f/k/a Lemon Squeezy LLC", meaning the legal entity was renamed, with f/k/a standing for "formerly known as". The footer of the technical documentation still reads "Lemon Squeezy, LLC". If you are signing a contract or entering a counterparty into your books, check which name appears in your own terms, because the vendor is not consistent here.
The practical conclusion: Lemon Squeezy is fine today for getting sales running, but not for planning five years ahead without an exit plan. Treat the integration as something you will rewrite onto Stripe Managed Payments or Paddle at some point. Keep order and subscription identifiers in your own database, for example in Supabase, instead of querying the vendor's API on every request.
Fees, surcharges and the arithmetic
The pricing headline is 5 percent plus 50 cents per transaction with no monthly fee. The fees documentation adds three surcharges that are easy to miss because they are absent from the pricing page:
- plus 1.5 percent for international transactions, meaning outside the United States,
- plus 1.5 percent for PayPal payments,
- plus 0.5 percent for subscription payments.
The fee is calculated on the total order value, so including the tax that goes to the authority anyway. That is not obvious and it raises the effective cost.
Payout fees come on top. A bank transfer via Stripe is free for US accounts and costs 1 percent for accounts outside the US. PayPal is 50 cents per payout for US accounts and 3 percent capped at 30 dollars for the rest. Payouts run twice a month. Marketing features are priced separately: plus 5 percent on payments recovered through abandoned cart emails, plus 3 percent on affiliate referrals on the merchant side, and plus 2 percent on affiliate payouts.
Let us run this on a 29 dollar monthly subscription sold to a customer in Poland, by card, paid out to a Polish bank account.
| Item | Amount |
|---|---|
| Product price | USD 29.00 |
| VAT at 23 percent | USD 6.67 |
| Order total | USD 35.67 |
| Fixed fee | USD 0.50 |
| 5 percent of 35.67 | USD 1.78 |
| 1.5 percent international transaction | USD 0.54 |
| 0.5 percent subscription | USD 0.18 |
| Platform fee total | USD 3.00 |
| Payable before payout fee | USD 26.00 |
| Payout fee, 1 percent | USD 0.26 |
| In your account | USD 25.74 |
You effectively hand over 3.26 dollars out of 29, that is 11.2 percent, not the 5 percent from the headline. The same calculation for a one-off 49 dollar sale to a US customer, with no state tax and a payout to a US account, gives a fee of 2.95 dollars, that is 6.0 percent. The spread between 6 and over 11 percent depends on where you sit and who you sell to.
The fees documentation offers its own example and its arithmetic checks out: a 20 dollar product, French VAT at 20 percent adds 4 dollars, order total 24 dollars, fee of 0.50 plus 1.20 plus 0.36 equals 2.06 dollars, payable 17.94 dollars. Note that for products cheaper than 10 dollars the fixed 50 cents eats such a large share of the price that the vendor itself sends those cases to its sales team for custom pricing.
Two vendor numbers disagree and I am giving both. The pricing page says 16 payment methods in one place and up to 21 in another. The same page cites support for 95 currencies in the comparison table and 130 plus in the questions section. There is no way to tell which figure is current, so assume the lower one when planning.
Lemon Squeezy, Paddle and Stripe side by side
The most important difference in this table is not in the percentages but in the row about tax liability.
| Feature | Lemon Squeezy | Paddle | Stripe in the standard setup |
|---|---|---|---|
| Merchant of record | yes | yes | no |
| Who is liable for VAT | the vendor | the vendor | you |
| Base fee | 5 percent plus 50 cents | 5 percent plus 50 cents | 1.5 percent plus PLN 1.00 for standard EEA cards |
| International cards | plus 1.5 percent | included | 3.25 percent plus PLN 1.00 |
| Currency conversion | included | included | plus 2 percent |
| Tax calculation | included | included | Stripe Tax, 0.5 percent per transaction |
| Filing returns | included | included | on your side |
| Payout fee | 1 percent outside the US | not in the price list | depends on country |
| Latest official SDK release | 4.0.0, 5 November 2024 | 3.10.0, 7 August 2026 | 22.5.0, 10 August 2026 |
The Stripe rates come from the price list served for Poland, because Stripe pricing depends on the account's country. What matters is the last row of the middle column together with the row about filing: with Stripe you pay less in percentage terms, but EU VAT registration, US nexus thresholds and monthly returns stay your problem. Stripe Tax will calculate the tax, yet it will not pay it for you or file anything. If you sell a dozen licences a month, that difference outweighs every percentage point in the table.
One player is missing from the table who, at the same entry rate, offers something Lemon Squeezy does not. Polar is also a merchant of record and also starts at 5 percent plus 50 cents, but it offers paid plans that lower the rate to 3.4 percent against a fixed monthly fee, so above a certain turnover it works out noticeably cheaper. It also charges no separate subscription surcharge. Two drawbacks need weighing: it is a far younger and smaller company with no Stripe backing, and its JavaScript package still carries a version number below 1.0, which means the interface may yet change.
Hosted checkout and the overlay
You have three ways to get a customer to pay. The simplest is a hosted checkout link generated in the dashboard and pasted anywhere. The second is the overlay, the same checkout opened in a modal above your page without leaving the app. The third is a checkout created through the API, when you want to override the price, prefill data or pass your own metadata.
You cannot build your own payment form. Card numbers are always collected by Lemon Squeezy, because it is the seller and it carries PCI responsibility. You can adjust colours and copy, not the layout.
import {
lemonSqueezySetup,
createCheckout,
} from '@lemonsqueezy/lemonsqueezy.js'
lemonSqueezySetup({
apiKey: process.env.LEMONSQUEEZY_API_KEY,
onError: (error) => console.error(error.message),
})
const { data, error } = await createCheckout(
process.env.LEMONSQUEEZY_STORE_ID!,
process.env.LEMONSQUEEZY_VARIANT_ID!,
{
checkoutData: {
email: user.email,
name: user.name,
custom: { user_id: user.id },
},
productOptions: {
redirectUrl: 'https://example.com/dashboard',
receiptThankYouNote: 'Your licence key is in this email.',
enabledVariants: [Number(process.env.LEMONSQUEEZY_VARIANT_ID)],
},
checkoutOptions: {
embed: true,
media: false,
subscriptionPreview: true,
buttonColor: '#111827',
},
testMode: process.env.NODE_ENV !== 'production',
expiresAt: null,
},
)
if (error) throw new Error(error.message)
const url = data?.data.attributes.urlThe checkoutData.custom field is what most people trip over. It lands in the webhook payload as meta.custom_data and it is the only reliable way to tie an order to a user in your database. Do not use the email address for that, because a customer may pay from a different one than they registered with.
The overlay needs the Lemon.js script and the global LemonSqueezy object. There are four methods plus two affiliate ones: Setup, Refresh, Url.Open, Url.Close, Affiliate.GetID and Affiliate.Build.
LemonSqueezy.Setup({
eventHandler: (event) => {
if (event.event === 'Checkout.Success') {
// event.data holds the order object, but trust the webhook instead
window.location.href = '/dashboard?pending=1'
}
},
})
LemonSqueezy.Url.Open(checkoutUrl)The available events are Checkout.Success, PaymentMethodUpdate.Mounted, PaymentMethodUpdate.Closed and PaymentMethodUpdate.Updated. In a single page application remember LemonSqueezy.Refresh() after a view change, because button listeners are attached once on load.
Webhooks and signature verification
Granting access in your application has to rest on a webhook, not on the post-payment redirect. A redirect can be faked by simply visiting that URL.
Lemon Squeezy signs the request with a secret you set yourself when creating the webhook, between 6 and 40 characters long. The signature arrives in the X-Signature header as a hex HMAC SHA-256 of the raw request body.
import crypto from 'node:crypto'
export async function POST(request: Request) {
const raw = await request.text()
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET!
const digest = Buffer.from(
crypto.createHmac('sha256', secret).update(raw).digest('hex'),
'utf8',
)
const signature = Buffer.from(
request.headers.get('X-Signature') ?? '',
'utf8',
)
if (
digest.length !== signature.length ||
!crypto.timingSafeEqual(digest, signature)
) {
return new Response('Invalid signature', { status: 401 })
}
const payload = JSON.parse(raw)
const eventName = payload.meta.event_name
const userId = payload.meta.custom_data?.user_id
if (eventName === 'subscription_created') {
// grant access to the user identified by userId
}
return new Response('ok', { status: 200 })
}Three things break this verification most often. The first is a parsed request body: a framework that calls JSON.parse for you will change key order or formatting and the signature stops matching, so you need the raw text. The second is timingSafeEqual, which throws when the buffers differ in length, which is why the length is checked first. The third is edge functions: in Next.js the webhook route should run on the Node runtime because it uses the node:crypto module, and on Vercel the default can differ from what you assume.
There are seventeen events: from order_created and order_refunded, through the full subscription cycle from subscription_created to subscription_expired, to license_key_created, customer_updated and affiliate_activated. A minimal sensible set for subscriptions is subscription_created, subscription_updated, subscription_payment_success and subscription_expired. The subscription_updated event is a catch-all and always carries current state, so if you handle only one, handle that one.
Send transactional email after granting access through your own channel, for example via Resend, because the built-in Lemon Squeezy mail module is priced separately by subscriber count and aims at newsletters rather than system notifications.
License keys and test mode
License keys are the feature that makes some teams pick Lemon Squeezy over a plain payment processor. After a purchase a key is generated, and a desktop application activates it under an instance name, which lets you cap the number of seats.
The License API is separate from the main API, has its own limit of 60 requests per minute instead of 300, and needs no API key, which lets you call it directly from a client application.
# activate a key on a specific machine
curl -X POST https://api.lemonsqueezy.com/v1/licenses/activate \
-H "Accept: application/json" \
-d "license_key=38b1460a-5104-4067-a91d-77b872934d51" \
-d "instance_name=Anna's MacBook Pro"
# validate on application startup
curl -X POST https://api.lemonsqueezy.com/v1/licenses/validate \
-H "Accept: application/json" \
-d "license_key=38b1460a-5104-4067-a91d-77b872934d51" \
-d "instance_id=47596ad9-a811-4ebf-ac8a-03fc7b6d2a17"The response contains the fields activated, error, license_key, instance and meta. Inside the license_key object you get status, activation_limit, activation_usage and expires_at. The status takes four values: inactive for a key with no activations, active with at least one, expired once the term passes or the subscription ends, and disabled after a manual switch-off in the dashboard.
Test mode is the default on a new account and covers the whole flow: checkout, subscriptions, license keys and webhooks. API keys are bound to a mode, so a test key never touches production data. Test cards include 4242 4242 4242 4242 for Visa, 4000 0000 0000 9995 for insufficient funds and 4000 0027 6000 3184 for 3D Secure. Two traps: file downloads are disabled in test mode, so you cannot test file delivery there, and products created in test mode do not move to live on their own, which is what the "Copy to Live Mode" option is for.
The SDK, its license and what is missing
Here comes the part that has to be said plainly. The official @lemonsqueezy/lemonsqueezy.js package sits at version 4.0.0, published on 5 November 2024. No release has appeared since, that is over 21 months of silence. I also checked the main branch of the lmsqueezy/lemonsqueezy.js repository: the package.json on main also reads 4.0.0, so there are not even unreleased changes waiting in the queue.
# state as of 22 August 2026
npm view @lemonsqueezy/lemonsqueezy.js version # 4.0.0
npm view @lemonsqueezy/lemonsqueezy.js time.4.0.0 # 2024-11-05
npm install @lemonsqueezy/lemonsqueezy.jsThe consequences are measurable, not theoretical. The Events type in the SDK lists fifteen webhook events while the API documentation lists seventeen. The missing ones are customer_updated and affiliate_activated. If you create a webhook through createWebhook and want to subscribe to either of them, TypeScript will reject a perfectly valid value. The workaround is trivial, a cast will do, but it tells you what state this library is in.
Beyond that the package is decent: 59 functions, full TSDoc types, no production dependencies, engines.node set to version 20 or newer, built in both ESM and CommonJS flavours. Function names are predictable, from listSubscriptions and updateSubscription to createUsageRecord and issueOrderRefund.
The license is one of the few places where everything lines up, and that deserves a mention because it often goes otherwise. I checked three sources. The LICENSE file on the repository's main branch carries the MIT text with the notice "Copyright (c) 2024 Lemon Squeezy, LLC". The license field in the npm registry reads MIT. The published package unpacks to 340 kB, contains package/LICENSE with the same text, and holds real code in dist/index.js and dist/index.cjs rather than a stub. Three sources, one answer, no surprises.
Mind the scope: MIT covers an HTTP client, not the platform. The service itself is closed and runs under terms the vendor can change. An open SDK license gives you no way whatsoever to run Lemon Squeezy on your own machines.
The ecosystem beyond the JavaScript package consists of an official Laravel package plus unofficial community libraries for Go, Ruby, Rust, Swift, Python, PHP, Elixir and Java. All of the latter are maintained by single individuals, so check the date of the last release before you put one into your go.mod or Gemfile.
Common mistakes
Granting access based on the post-payment redirect. Anyone who guesses the redirectUrl can open it. Grant entitlements only after a verified webhook, and treat the redirect as a "processing" screen.
Matching accounts by email address. A customer pays from a work address while having registered with a personal one, and the account ends up with no subscription. Pass the user identifier through checkoutData.custom and read it from meta.custom_data.
Forecasting revenue from the pricing headline. Selling from Europe into Europe, the real cost is around 11 percent, because the international, subscription and payout surcharges stack up. Budget with the number computed for your own case, not with 5 percent.
Parsing the request body before checking the signature. Middleware that turns JSON into an object automatically invalidates the HMAC. You need the raw text before anything touches it.
No handling of redeliveries. The same webhook can arrive twice. Store the event identifier and make handling idempotent, otherwise a single payment will credit two billing periods.
Treating the mail module as an authorisation system. Lemon Squeezy knows about orders but knows nothing about sessions in your app. Login and permissions stay on your side, in Better Auth or Clerk, and payment is only a flag attached to an account.
Postponing the exit plan. Since the vendor itself talks about migrating to Stripe Managed Payments, keep the payment layer behind your own interface with two or three methods. Swapping the implementation then takes a day rather than a quarter.
FAQ
Is Lemon Squeezy still accepting new customers?
Yes. As of 22 August 2026 signup works, pricing is published and documentation is being updated. There is no announced shutdown date. There is, however, an official post from 28 January 2026 about building Stripe Managed Payments and an intent to give users an easy migration, so plan on the assumption that you will move eventually.
How much do I actually keep from a sale?
On a 29 dollar subscription sold to a customer in Poland, with 23 percent VAT and a payout outside the US, 25.74 dollars lands in your account, meaning the cost is 11.2 percent of the net price. On a one-off 49 dollar sale to a US customer paid out to a US account, the cost drops to 6.0 percent.
How does Lemon Squeezy differ from Stripe?
Lemon Squeezy is a merchant of record and takes on calculating, filing and paying VAT and sales tax. Stripe in the standard setup is a payment processor, so tax obligations stay with you, and Stripe Tax only calculates the tax for an extra 0.5 percent per transaction. The percentage gap matters less than the liability gap.
Can I build my own payment form?
No. Card details are always collected by Lemon Squeezy, because it is the formal seller. Your options are the hosted checkout, the Lemon.js overlay, and an API-created session with an overridden price and prefilled data. You customise colours, copy and element visibility, not the layout.
Does an SDK with no release since 2024 rule the platform out?
It does not rule it out, but factor it into the cost. The API is versioned with the /v1 prefix and promises backwards compatibility, so existing calls keep working. The gaps show up in the types, for instance two webhook events are missing from the Events type. If you would rather not work around that, call the API directly with fetch.
Are license keys enough to protect a desktop application?
They are enough for seat accounting and for revoking access, since activation_limit together with activation_usage cap the number of instances and a key can be disabled from the dashboard. They are not protection against cracking, because validation happens on the client and the 60 requests per minute limit forces you to cache the result locally.