# Polar sandbox to production, the checklist that stops launch-day silence

> Sandbox and production are separate Polar deployments with separate tokens, product ids and webhook secrets. Nothing carries over. Everything that has to be recreated, in order, and how to prove it worked.

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

1. **Create the production organization** at https://polar.sh and finish
   onboarding.
2. **Create a production token.** Settings, Developers, New Token. Sandbox
   tokens do not work on the production host, and the reverse.
3. **Create the products from the catalogue.** With the production token and
   `POLAR_ENVIRONMENT=production` loaded:

   ```bash
   bun run billing:sync-plans
   ```

   It creates one product per catalogue price (Polar sells one price per
   product) with `price_id` metadata, and prints the five `BILLING_PRICE_*`
   lines. The ids are new UUIDs. That is fine: no id is written in code.
4. **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.ts` accepts both that and
   the older format, so paste it as shown.
5. **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_ENVIRONMENT` picks the API host. A production token with it left at
   `sandbox` fails to authenticate; a sandbox token with `production` does the
   same the other way. Both are quiet until someone clicks Buy.
6. **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.
7. **Run `bun run verify`** with the production values. The Polar check
   asserts the token belongs to that environment and organization, and that
   every `BILLING_PRICE_*` points at a live product with the catalogue's
   amount, currency and interval.
8. **Buy your own product with a real card.** The smallest price you sell.
   Watch the chain: the order in the dashboard, `200` in the endpoint's
   delivery log, the row in `billing_subscriptions` or `billing_purchases`, and
   `/billing` showing 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:reconcile` to 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.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
