The failure looks like this. You ship on a Friday. Checkout loads, cards go through, money appears in the Polar dashboard, and nobody's account unlocks. No error in the logs, nothing red anywhere.
The cause is almost always one value in production that still points at
sandbox, or one thing that exists in sandbox and was never created in
production. Polar keeps the two apart: a different API host
(sandbox-api.polar.sh against api.polar.sh), dashboard, organization,
tokens, product ids, webhook endpoints and signing secrets. Production is a
second setup, not a switch.
Before launch day
Start merchant onboarding early. Polar is the seller, so it needs your business details, and a person reviews them. Start days before you need it. Until it completes, your production organization cannot take money.
Know what you sell. In this repo the answer is one file:
src/lib/pricing.ts. Every plan and price is there, so there is no sandbox
catalogue to copy by hand.
The checklist
Create the production organization at https://polar.sh and finish onboarding.
Create a production token. Settings, Developers, New Token. Sandbox tokens do not work on the production host, and the reverse.
Create the products from the catalogue. With the production token and
POLAR_ENVIRONMENT=productionloaded:bun run billing:sync-plansIt creates one product per catalogue price (Polar sells one price per product) with
price_idmetadata, and prints the fiveBILLING_PRICE_*lines. The ids are new UUIDs. That is fine: no id is written in code.Create the webhook endpoint. Settings, Webhooks, Add endpoint:
- URL
https://<your-domain>/api/webhooks/polar - Format Raw. Discord and Slack formats sign a chat message, and the handler answers 500 with a note to switch.
- Events:
order.paid,order.refunded,subscription.created,subscription.updated,subscription.active,subscription.canceled,subscription.uncanceled,subscription.revoked,subscription.past_due.
Copy the signing secret. Endpoints made since 8 Sep 2026 sign with Standard Webhooks;
src/lib/billing/polar-webhooks.tsaccepts both that and the older format, so paste it as shown.- URL
Set the environment in your host, for the production environment only:
POLAR_ACCESS_TOKEN=<production token> POLAR_WEBHOOK_SECRET=<production endpoint secret> POLAR_ORGANIZATION_ID=<production organization id> POLAR_ENVIRONMENT=production BILLING_PRICE_PRO_MONTHLY=... (and the other four)POLAR_ENVIRONMENTpicks the API host. A production token with it left atsandboxfails to authenticate; a sandbox token withproductiondoes the same the other way. Both are quiet until someone clicks Buy.Deploy, with migrations run. The billing tables must exist before the first webhook lands, or the handler answers 500 and Polar retries into a table that is not there.
Run
bun run verifywith the production values. The Polar check asserts the token belongs to that environment and organization, and that everyBILLING_PRICE_*points at a live product with the catalogue's amount, currency and interval.Buy your own product with a real card. The smallest price you sell. Watch the chain: the order in the dashboard,
200in the endpoint's delivery log, the row inbilling_subscriptionsorbilling_purchases, and/billingshowing the plan. Refund yourself and watch the plan go away.
That last step is the only one that proves the other seven.
Easy to miss
- Preview deployments often inherit production variables. A preview with a production token can take real money against a branch. Scope production values to production and give previews the sandbox set.
- The webhook route is public by design. The signature protects it. Do not put it behind deployment protection or a password: Polar cannot log in, and the deliveries fail with a 401 that looks nothing like a signature problem.
- The delivery log answers most launch-day questions. Settings, Webhooks,
your endpoint: every attempt, its status and its body, with a redeliver
button. A redelivery of a handled event answers
duplicate, which is fine. - Ten failures in a row disable the endpoint. A wrong secret on launch day
can switch it off before you notice. Re-enable it after the fix and run
bun run billing:reconcileto pick up what it missed. - Schedule the nightly jobs (
billing:reconcile,billing:prune-events) in production too. Polar sends no dispute webhook; reconcile is what revokes a charged-back lifetime deal.