Environments
Sandbox vs live — hosts, environment-scoped keys, and how to tell where a request landed
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
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 onsandbox.billwave.examplewith 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.
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
An API key belongs to exactly one environment, encoded in its prefix. The API enforces it:
- an
billwave_sk_test_…key sent toapi.billwave.exampleis rejected with 401environment_mismatchbefore anything is written; - an
billwave_sk_live_…key sent tosandbox.billwave.exampleis rejected the same way.
{
"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.
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.
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 |
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
Neither the SDK nor the CLI defaults to an environment silently.
SDK
// 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.
CLI
billwave sync # mode inferred from an billwave_sk_test_/billwave_sk_live_ key
billwave sync --mode live --yes # explicit
BILLWAVE_MODE=sandbox billwave diffWith a legacy key and no --mode, billwave exits with code 2 and says what to pass. See CLI commands.
Same code, both environments
Keep one code path and switch only the key:
# .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:
BILLWAVE_SECRET_KEY=$SANDBOX_KEY billwave sync --yes
BILLWAVE_SECRET_KEY=$LIVE_KEY billwave sync --yesSelf-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.