Skip to main content

Split staging and production across subaccounts

The migration moment most multi-environment questions reduce to is: I’ve been running everything on one organization, and I now want a prescriptive recipe that puts test sends on one subaccount, live sends on another, with separate API keys, separate rate limits, and a clean chargeback line per environment at month-end. This guide is that recipe, end to end. It is tenant-owned throughout — nothing below changes how outbound traffic terminates; it changes who bills against whose wallet. The shape you are aiming for:
By the end, each environment runs under its own subaccount id and its own scoped API key. Any spend is attributed at the subaccount row, so a staging regression run can never leak into the production line on your statement.

1. When to split

Pick this shape when one or more of these is true:
  • Separate test, staging, and production traffic. One subaccount per environment keeps regression sends, soak tests, and live sends from competing on the same rate limit or wallet. The sibling multi-environment subaccounts page walks through the flat per-stage model; this guide is the migration recipe into it.
  • Department-level billing. Each business unit gets its own child so spend lines can be invoiced or transferred per unit — the parent statement stays the single source of truth.
  • Brand-level channels. A multi-brand org isolates one brand per subaccount so that sender IDs, WhatsApp templates, and AI configuration stay per brand and a leaked key on one brand can’t pivot across the rest.
A split is the right call once any one of these is a real operational load. If you are only experimenting, tag sends with metadata.cost_center first — the cost-center tag gives you per-department attribution on one org without a structural change.

2. Plan the tree

Draw the mapping before you touch the API. The three axes you need to settle are subaccounts, channels, and keys, and the plan is a table, not a diagram: Two exercise-the-plan rules:
  1. Senders and numbers scope to a subaccount, not the parent. When you assign a sender to a child, that sender is reachable only by the child’s keys. Plan explicitly which senders belong to every child — and expect to keep a “test sender” distinct from the production set.
  2. Keys are per subaccount. A parent-scoped key mints, funds, and retires children; a child-scoped key operates only that child’s messaging. The tree is unambiguous about which key goes where.
See Concepts — subaccount–organization model for the underlying tenancy shape.

3. Create the subaccounts

You have two surfaces. The dashboard is the right choice when a human approves each step; the API is the right choice when this is part of an automated onboarding run. Dashboard — open Settings → Subaccounts → New subaccount (/settings/subaccounts/new) and walk the wizard through once per environment. The step-by-step contract (Basics → Brand → Domain → Connections → Credits → Review) is documented in New subaccount wizard. API — one call per subaccount:
Run it once per environment (payments-staging, payments-production). The response carries the child id (sub_…) you carry into every later step. pending_setup: true keeps the child out of a billing-active state until you finalize it — see 5-step wizard for the finalize call. Assign senders and numbers next, then fund each child from the parent wallet with POST /api/v1/subaccounts/{id}/transfer-credits (owner role only). The parent’s wallet is the feeder — traffic on each child draws only from that child’s balance, and an empty staging child stops sending without touching production.

4. Per-subaccount API keys

Mint a scoped key per child, never a single shared key across environments:
Two kinds of scoping stack on each other:
  • Which organization the key can see. A key minted on sub_stage9x calls /sms, /messages, analytics, and AI endpoints as that child — it cannot read the parent’s tree or the sibling’s data. A 403 on POST /api/v1/subaccounts from a child key and a 404 on a child-scoped read from a parent key are the expected isolation failure modes and a feature, not a bug.
  • What that key can do. Scope it down to messages:read + messages:write (or whichever family the surface needs) so a leaked key is bounded. See Scope API keys per integration for the scope-by-surface matrix.
Rate limits and billing attribute up the tree by design. A key minted on a child draws against that child’s rate limit and that child’s spend cap, and its usage rolls up into the parent’s per-subaccount line items on the consolidated statement. See Per-API-key usage limits for the per-key rate-limit and monthly-quota knobs you set inside a child.

5. Connection modes per child

Per channel, decide whether each subaccount sends on the platform default connection or on a connection you bring (your own SIP trunk, SMPP account, sending domain, WhatsApp Business account, and so on). The rule is: parent picks it per channel, and every child inherits. There is no per-subaccount override. The full model — the 11 provider types, the master reseller toggle, credential rotation, and fallback to platform default — is in Per-channel connection mode. For a staging vs production split, the two common patterns are:
  • Both children inherit the platform default. Simplest; everything meters at standard rates against the parent wallet.
  • Production on a BYO connection, staging on platform default. Set the BYO row on the parent once (it covers every child), then constrain staging’s spending by its monthly cap and rate limit so the BYO carrier never sees test traffic ramp.
Pair connection modes with the white-label subaccounts page when you also operate children for customers — the reseller flow is one level of indirection on top of what you build here.

6. Apply AI configuration per subaccount

If the environments behave differently on AI — a staging child that never runs paid models, a production child pinned to a premium provider — set it on GET/PUT /api/v1/subaccounts/{id}/ai-config per child:
The full field contract (the stored vs resolved view, reset-by-null semantics, the comped marker) is in Per-subaccount AI configuration. Do not try to control spend with enabled: false; that gates the feature. Use the monthly spend cap and per-key quota for spend control.

7. Move traffic with a blue/green cut

Re-point an existing integration without a freeze window:
  1. Verify the children are healthy. Confirm each child shows active under Settings → Subaccounts, that its scoped key mints, and that a smaller-than-usual test send succeeds on the staging child.
  2. Dual-send for a full business day. Point your integration’s staging traffic at the staging child’s key. Keep live traffic on the existing key. Read GET /api/v1/subaccounts/usage-rollup and confirm the staging line is growing and is the only line that grows.
  3. Cut production to the new key. Deploy the integration with the production child’s key. Watch the parent’s usage rollup: the production line ticks up as the old line ticks down. A falling line where you expect a rising one means a key is pointing at the wrong child.
  4. Quiesce the old keys. Leave the parent-scope integration key in place for one more day for emergency rollback, then rotate it out. Stale keys are the most common production leak after a cut — a worker that still has the old credential keeps billing against the old wallet, invisible from the new subaccount’s dashboard.
If you run canary deploys, route 5% of live sends to the production child’s key first, verify the rollup line grows proportionally, then widen. The usage rollup (see next section) is the monitor you watch throughout; you do not need a separate observability wiring for the cut itself.

8. Bill each subaccount separately

At month-end the parent reads one rollup line per child and reconciles against the existing chargeback surfaces:
  • Per subaccount usage. GET /api/v1/subaccounts/{id}/usage returns one child’s metered usage, and GET /api/v1/subaccounts/usage-rollup folds the whole tree. That is your staging-vs-production statement.
  • AI spend per child. Open Billing → Usage → AI in the dashboard or call GET /api/v1/usage/ai scoped per subaccount — the surface is the same shape as the org-wide AI usage split, just on the child.
  • Intra-environment chargeback. When one child splits its own spend further (say, stage_regression vs stage_ux), the metadata.cost_center tag still applies inside the child — tag at send time, read the rollup, and the parent’s per-child line stays as the outer wallet boundary.
The statement exporter serves the same period as CSV or JSON:
  • GET /api/v1/billing/statements — monthly summaries
  • GET /api/v1/billing/statements/:period — a month’s detail rows
  • GET /api/v1/billing/statements/:period/download — CSV or JSON export per period
Together, the usage rollup + AI split + chargeback tagger + the statement export give you a per-environment invoiceable artifact without contacting support.

9. Common pitfalls

  • Channel leakage. Senders not scoped to a child are unreachable from it (the “404 on a child-scoped read” isolation failure) — but senders left on the parent keep leaking traffic there. Audit every sender after the split and move the test senders explicitly; the staging-versus-production sender split is what makes the billback real.
  • Template sharing does NOT cross subaccounts. A WhatsApp or SMS template approved on one child is not visible to the sibling. Recreate the template on each child, or plan a shared template review pass per child as part of onboarding. Subaccounts API covers the per-child template surface.
  • Billing surprises. Two places staging invoices itself into production: a stale pending child never finalized (it cannot serve traffic but also shows in the list — either finalize it or delete it), and a stale parent-scoped integration key still bound in a worker. The first shows in Settings → Subaccounts with status pending; the second shows up as a usage line on the parent rollup when every other line is where you expect it.
  • Budget ceilings. The child’s monthly_spend_cap_cents and its rate limit are what they say they are — staging does not borrow from production’s headroom. A staging regression run that needs headroom you did not plan needs a cap raise, not an emergency credit transfer.

See also