You launch. A customer pays. The money shows in the dashboard. Their account never unlocks, and nothing is red.
With Lemon Squeezy the cause is almost always one value still pointing at test mode, or one thing that exists in test mode and was never created in live mode.
What is separate
| Thing | Test and live share it? |
|---|---|
| Store and store id | Shared |
| API keys | No. A key created in test mode only sees test data |
| Webhooks and signing secrets | No. Test webhooks fire only for test data |
| Products and variants | No. "Copy to live mode" makes new variant ids |
| Orders, subscriptions, customers | No |
The store id is the one value that stays the same, which is why it is the one people forget to check the others against.
The failure modes, loudest first
- Live key, test variant ids. Checkout creation fails because the variant does not exist in live mode. Loud.
- Test key, live settings. Every checkout is a test checkout. Nobody is charged, and nobody notices until the first payout is empty.
- Everything live except the webhook. The live webhook was never created, or points at a preview URL, or has the test secret. Payments succeed, nothing lands, every page still shows the free plan. Silent. This is the one.
- Live webhook missing an event. Deliveries are all green. The events you did not tick never arrive, so cancellations or refunds never reach your database. Silent for weeks.
Make the mismatch impossible to miss
- Keep a mode flag with test as the default. An env var such as
LEMONSQUEEZY_MODE=test. A fresh clone cannot take real money. - Check the key's real mode.
GET /v1/users/me(getAuthenticatedUser) returnsmeta.test_modefor the key. Compare it with your flag in a verify script and fail loudly when they differ. - Pass the mode to checkout.
createCheckout(store, variant, { testMode })keeps a mismatched deploy from charging a real card in the wrong world. - Drop webhooks from the other mode. Every payload has
meta.test_mode. If it disagrees with your flag, write nothing. A test purchase must never entitle anyone on production. - Keep variant ids in env, never in code. One env var per price
(
BILLING_PRICE_PRO_MONTHLY). Test and live get different values from the same build. - Check every id against the catalogue. A script that fetches each variant
(with its price model and product) and compares kind, amount, currency,
interval, trial, store and
test_modeturns a pasted test id on production into a failed deploy check instead of a failed checkout.
Launch-day checklist
- Store activated (identity and payout details approved). Live checkouts are refused until then.
- Products copied to live mode, published, with the right tax category.
- Test mode switched off. Live API key created. The test key stays in local
.env.localonly. - Live webhook created at
https://<your-domain>/api/webhooks/lemonsqueezywith a new signing secret and every event your handler lists. - Production env: API key, webhook secret, store id, mode
live, and the live variant id for every price. - Run your verify script against production's env. It should report live mode and every price matching.
- Deploy. Buy your own product with a real card. Confirm the plan shows up. Refund yourself. Confirm it goes away.
- Subscribe, then cancel in the portal. Confirm the app shows the end date.
Step 8 is the one people skip, and it is the only step that proves the live
webhook receives more than order_created.
After launch
Keep test mode working. Point a staging deployment at the test key, the test webhook and the test variant ids. When you add an event or a price, do it in test mode first, then repeat it in live mode the same day. Two lists that must match drift the first time someone updates only one.