Billwave
Get Started

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.

EnvironmentHostKey prefixUse it for
Sandboxhttps://sandbox.billwave.examplebillwave_sk_test_Development, tests, CI — throwaway data
Livehttps://api.billwave.examplebillwave_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 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.

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 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.
{
  "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:

WhereField / headerValues
Response headerX-Billwave-Environmentsandbox | live
Response headerX-Billwave-Organizationyour organization ID
check / track bodyenvironmentsandbox | 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 diff

With 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 --yes

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.

On this page

Ask about billwave

Ready

Start a new chat below.

Powered by Cull