# Dodo test mode and live mode are two separate worlds

> Different API keys, webhook keys, product ids and customers. Nothing carries over, and the mismatch that hurts most is silent.

You ship. Checkout loads. A customer pays. Money appears in the dashboard. Their
account does not unlock, and nothing anywhere is red.

Almost every time, the cause is one of two things: a value in production still
points at test mode, or something that exists in test mode was never created in
live mode.

Dodo keeps the two apart: separate API keys and hosts, separate webhook
endpoints and signing keys, separate products with separate ids, separate
customers. `DODO_PAYMENTS_ENVIRONMENT` picks the host. Live mode is a second
setup, not a switch.

## The failure modes, from loud to silent

**Test key with `live_mode`, or the reverse.** Every API call answers 401.
Checkout lands on `/billing` with "Checkout could not start". Loud, fixed in a
minute, and `bun run verify` names it.

**Right key, right mode, test product ids.** The `BILLING_PRICE_*` values still
point at test-mode products, which do not exist in live mode. Checkout fails
the same loud way. `verify` names the env vars.

**Right everything, wrong webhook key.** The expensive one. Checkout works. The
card is charged. Every webhook delivery gets a 400 from your endpoint. A buyer
who comes back to the success page still gets their plan (the page asks Dodo
directly), so it looks fine when you test it. But nothing else is ever
recorded: not the buyer who closed the tab, not a renewal, not a cancellation,
not a refund. Only the endpoint's delivery log shows it.

**Right everything, endpoint missing events.** Same symptom, different cause:
the endpoint answers 200 to the events it was subscribed to and never sees the
rest. A cancellation from the portal never arrives, and the customer keeps
paid features.

## The environment variables

```
DODO_PAYMENTS_API_KEY=<mode-specific>
DODO_PAYMENTS_WEBHOOK_KEY=<endpoint-specific>
DODO_PAYMENTS_ENVIRONMENT=test_mode | live_mode
BILLING_PRICE_PRO_MONTHLY=pdt_...   (one per catalogue price, mode-specific)
```

- The default is `test_mode` (`src/lib/billing/dodo-config.ts`). The worst case
  for a misconfigured clone should be a checkout that charges nobody.
- The webhook key is **per endpoint**. The CLI relay endpoint, your tunnel
  endpoint and production each have their own.
- Set production values in your host's environment, scoped to production.
  Preview deployments often inherit production variables by default, which
  means a preview URL can take real money. Give previews the test-mode set.

## One database, one mode

Customers are mode-specific too, and this repo stores one Dodo customer per
user in `billing_customers`. Point a live deploy at a database that holds
test-mode mappings and the first checkout fails with an unknown customer. Use
a separate database per mode (you almost certainly do), or clear
`billing_customers` when you switch a database from test to live.

## Going live, in order

1. **Finish business verification and add the payout account.** Dodo is the
   merchant of record and a person reviews it. Start days ahead.
2. **Create a live API key**, set it with `DODO_PAYMENTS_ENVIRONMENT=live_mode`.
3. **Create the live products**: `bun run billing:sync-plans` with the live
   key. It creates one product per catalogue price, with the tax category in
   `DODO_TAX_CATEGORY`, and prints the live `BILLING_PRICE_*` lines. Put them in
   production's environment.
4. **Create the live webhook endpoint** at
   `https://<your-domain>/api/webhooks/dodo`, subscribed to every event in
   `HANDLED_EVENTS` (`src/lib/billing/dodo-events.ts`). Copy its signing key.
5. **Deploy, then run `bun run verify`** against production's values. It
   checks the key works in the configured mode, the webhook key is a real
   `whsec_` key, and every price points at a live product with the catalogue's
   amount and interval.
6. **Buy your own lifetime product with a real card.** Check, in order: the
   payment in the Dodo dashboard, a `200` in the endpoint's delivery log, a row
   in `billing_purchases`, the plan on `/billing`. Refund yourself and confirm
   the plan goes away.

Step 6 is the only one that proves the other five.

## Debugging when it is already broken

Go to the delivery log first: **Developer -> Webhooks -> your endpoint**. It
shows every attempt, the response code and the body, and you can resend any of
them.

- **400 on every delivery**: wrong `DODO_PAYMENTS_WEBHOOK_KEY`. The body says
  "Invalid signature".
- **401 or 403 from something else**: deployment protection or a WAF in front
  of the route. Dodo cannot log in.
- **404**: wrong path, or the deploy with the route never shipped.
- **500**: the handler threw. "Webhook not configured" means the key is unset;
  anything else, read the server log (a missing migration is the classic).
- **200 with `"outcome":"ignored"`**: the event type is not in
  `HANDLED_EVENTS`, so it was acknowledged and dropped.

Once it is fixed, resend the failed deliveries or run
`bun run billing:reconcile`. The handlers re-read state from Dodo, so both
are safe and enough. You do not rebuild anything by hand.

---

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
