Skip to content

Moving a Lemon Squeezy integration from test mode to live mode

Test and live are separate worlds with separate keys, webhooks and variant ids. The checklist, and the silent mismatch that sells nothing.

Lemon Squeezy4 min readships at docs/solutions/lemonsqueezy/test-mode-to-live-mode.md

Tags: lemonsqueezy · test-mode · live-mode · launch · checklist · environments

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

ThingTest and live share it?
Store and store idShared
API keysNo. A key created in test mode only sees test data
Webhooks and signing secretsNo. Test webhooks fire only for test data
Products and variantsNo. "Copy to live mode" makes new variant ids
Orders, subscriptions, customersNo

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

  1. Live key, test variant ids. Checkout creation fails because the variant does not exist in live mode. Loud.
  2. Test key, live settings. Every checkout is a test checkout. Nobody is charged, and nobody notices until the first payout is empty.
  3. 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.
  4. 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) returns meta.test_mode for 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_mode turns a pasted test id on production into a failed deploy check instead of a failed checkout.

Launch-day checklist

  1. Store activated (identity and payout details approved). Live checkouts are refused until then.
  2. Products copied to live mode, published, with the right tax category.
  3. Test mode switched off. Live API key created. The test key stays in local .env.local only.
  4. Live webhook created at https://<your-domain>/api/webhooks/lemonsqueezy with a new signing secret and every event your handler lists.
  5. Production env: API key, webhook secret, store id, mode live, and the live variant id for every price.
  6. Run your verify script against production's env. It should report live mode and every price matching.
  7. Deploy. Buy your own product with a real card. Confirm the plan shows up. Refund yourself. Confirm it goes away.
  8. 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.