# Environments (https://docs.billwave.example/getting-started/environments)

Environments [#environments]

Billwave runs two completely isolated environments. Nothing is shared between them: customers, plans, subscriptions, usage and provider accounts all live in one or the other.

| Environment | Host                               | Key prefix          | Use it for                                |
| ----------- | ---------------------------------- | ------------------- | ----------------------------------------- |
| **Sandbox** | `https://sandbox.billwave.example` | `billwave_sk_test_` | Development, tests, CI — throwaway data   |
| **Live**    | `https://api.billwave.example`     | `billwave_sk_live_` | Production — real customers, real charges |

The dashboard has a sandbox/live toggle in the header; everything you see is scoped to the selected environment.

Sandbox is managed [#sandbox-is-managed]

You never configure a payment provider for the sandbox. Billwave owns a test account on every supported provider (Paystack, Stripe, Dodo Payments, Bachs) and every organization transacts through them:

* `attach()`, wallet setup, invoice payment and plan sync all just work on `sandbox.billwave.example` with a fresh organization.
* Checkout pages are the providers' real test pages — use their test cards.
* Provider webhooks for those accounts are already pointed at Billwave, so payments confirm without you registering anything.
* **Settings → Providers** lists these as **Managed by Billwave**. They cannot be edited or deleted.

If you need to see sandbox activity in *your own* provider dashboard, add your own test keys for that provider under **Settings → Providers**. Your account then replaces the managed one for that provider only, and you register the per-organization webhook URL yourself as described in [Webhook setup](/getting-started/webhook-setup).

**Live is yours.** Production always charges through provider accounts you own. The **Go to Production** flow in the dashboard asks for live keys for the providers your sandbox catalog uses before it lets you switch.

Keys are scoped to one environment [#keys-are-scoped-to-one-environment]

An API key belongs to exactly one environment, encoded in its prefix. The API enforces it:

* an `billwave_sk_test_…` key sent to `api.billwave.example` is rejected with **401 `environment_mismatch`** before anything is written;
* an `billwave_sk_live_…` key sent to `sandbox.billwave.example` is rejected the same way.

```json
{
  "success": false,
  "error": {
    "code": "environment_mismatch",
    "message": "This API key is scoped to the sandbox environment but the request was sent to the live API (https://api.billwave.example). Use an billwave_sk_live_… key, or send this request to https://sandbox.billwave.example."
  },
  "environment": "live",
  "keyEnvironment": "sandbox"
}
```

Create one key per environment in **Settings → API keys**, choosing the environment for each. `billwave connect` issues both at once.

<Callout type="info">
  **Legacy keys.** Keys created before scoping existed look like `billwave_sk_…`
  (no `test`/`live`) and are still accepted by both hosts. They cannot protect
  you from writing to the wrong environment — rotate them when you can.
</Callout>

Every response tells you where it landed [#every-response-tells-you-where-it-landed]

You never have to infer the environment from a hostname. Every public API response carries:

| Where                  | Field / header            | Values               |
| ---------------------- | ------------------------- | -------------------- |
| Response header        | `X-Billwave-Environment`  | `sandbox` \| `live`  |
| Response header        | `X-Billwave-Organization` | your organization ID |
| `check` / `track` body | `environment`             | `sandbox` \| `live`  |

```ts
const result = await billwave.track({ customer: "workspace_1", feature: "agent_turns" });
if (result.environment !== "live") {
  console.warn("usage was recorded in", result.environment);
}
```

Selecting the environment [#selecting-the-environment]

Neither the SDK nor the CLI defaults to an environment silently.

SDK [#sdk]

```ts
// A scoped key is enough — the SDK infers the environment from the prefix
const billwave = new Billwave({ secretKey: process.env.BILLWAVE_SECRET_KEY });
billwave.mode; // "sandbox" | "live"

// A legacy key needs an explicit mode
const billwave = new Billwave({ secretKey: "billwave_sk_…", mode: "sandbox" });
```

With a legacy key and no `mode`, the first request throws `BillwaveError` (`code: "config_error"`) rather than hitting live. If `mode` contradicts the key's scope, that is also a `config_error`. See [Configuration](/sdk/configuration).

CLI [#cli]

```bash
billwave sync                     # mode inferred from an billwave_sk_test_/billwave_sk_live_ key
billwave sync --mode live --yes   # explicit
BILLWAVE_MODE=sandbox billwave diff
```

With a legacy key and no `--mode`, `billwave` exits with code `2` and says what to pass. See [CLI commands](/cli/commands).

Same code, both environments [#same-code-both-environments]

Keep one code path and switch only the key:

```sh
# .env.development
BILLWAVE_SECRET_KEY=billwave_sk_test_…

# .env.production
BILLWAVE_SECRET_KEY=billwave_sk_live_…
```

Sync your catalog to each environment separately — the catalog is data, and the two environments don't share it:

```bash
BILLWAVE_SECRET_KEY=$SANDBOX_KEY billwave sync --yes
BILLWAVE_SECRET_KEY=$LIVE_KEY    billwave sync --yes
```

Self-hosted deployments [#self-hosted-deployments]

If you run your own Billwave API, point the SDK at it with `apiUrl` and the CLI with `BILLWAVE_API_URL` (or `environments.test` / `environments.live` in `billwave.config.ts`). The key prefix rules still apply, because the same code enforces them.

Related [#related]

* [API Keys](/getting-started/api-keys)
* [SDK Configuration](/sdk/configuration)
* [CLI Commands](/cli/commands)