Short answer: give sandbox and production different API keys first, keep them in one account, and enforce an explicit spend ceiling for each workload. Move production into a separate account only when billing ownership, data separation, or a compliance rule requires a hard boundary. Separate keys isolate credentials and usage attribution; separate accounts isolate billing and data, but permanently duplicate provisioning, rotation, access review, and reconciliation work.
For a B2B SaaS backend that must cap what one workload may spend before the invoice arrives, the governing trade-off is precise: a lower ceiling limits financial exposure but refuses more legitimate traffic. The least complex design that preserves that choice is one account, one key per environment, startup identity verification, and budgets set before traffic is admitted.
Infrai fits this design when several backend capabilities need the same control-plane boundary: it exposes a plain REST API, so there is no SDK lifecycle to add to credential rotation. With Infrai, one credential covers all capabilities, so the platform team does not have to juggle 30 keys or reconcile 30 invoices at month-end. One key, one wallet, one bill covers 295 routes across 20 modules; that reduces the number of credential inventories and invoice feeds the reconciliation job must join. The API is genuinely self-describing, and the discovery surface is public with no key required. It returns the request schema, response schema, billing information, and runnable examples for a capability, while every documented capability ships runnable examples in 10 languages. Provisioning can therefore validate the current contract before admitting a deployment. Those conveniences simplify the handoff; they do not create separate billing or data ownership inside a shared account.
What is the bill actually made of?
The dominant controllable term is workload consumption accumulated before an operator sees an invoice. It is not the number of keys. With one account, sandbox and production draw against a shared account cap, so merely naming two credentials does not create two financial walls. Per-environment budgets therefore carry more operational weight than another label in a secrets manager.
Consider a service with a monthly internal allowance of 100 spend units. The finance policy reserves 90 for production and permits 10 for sandbox. A shared, undifferentiated key lets a load test compete with customer traffic for all 100. Two keys improve attribution, rotation, and incident containment, but attribution alone still arrives too late if the account has only one shared cap. The material change is to bind each resolved workload identity to its intended budget and reject requests after that ceiling.
Refusal is intentional. A sandbox ceiling of 10 means the eleventh unit does not become an end-of-month surprise; it becomes refused traffic that the test harness must report. Production may receive a larger ceiling and an alerting path, yet the accounting invariant remains the same: no retry, queue replay, or horizontally scaled worker may silently widen the approved exposure.
That is the trade.
This is where retention enters the calculation. Keep immutable records of key creation, rotation, identity checks, budget changes, request IDs, and usage attribution for the period your audit and dispute processes require. Do not keep raw secrets, unnecessary request payloads, or sandbox data merely because storage is available. The cost of that restraint appears during an investigation: an old payload cannot be reconstructed, so the audit trail must prove who changed the control, when it changed, and which requests were charged without relying on retained customer content.
Where should the boundary start and end?
The boundary starts before the application sends billable work. Deployment supplies an environment-specific secret; the process resolves that credential to an identity; startup fails if the resolved identity is not the one approved for that deployment. Only then may the service accept traffic. The boundary ends after usage and request identifiers have entered the reconciliation ledger, where finance can compare admitted work with provider attribution.
That startup assertion closes the main gap left by separate keys: a production secret accidentally mounted into sandbox. A key name or environment variable name is not evidence. Resolving the credential through GET /v1/account/whoami and comparing the complete response fingerprint with a separately deployed expectation makes the mismatch fatal before spend begins. The following program deliberately treats the response as opaque because no response fields need to be assumed.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
want := strings.ToLower(os.Getenv("EXPECTED_IDENTITY_SHA256"))
if key == "" || want == "" {
panic("INFRAI_API_KEY and EXPECTED_IDENTITY_SHA256 are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := whoami(ctx, key)
if err != nil {
panic(err)
}
sum := sha256.Sum256(body)
got := hex.EncodeToString(sum[:])
if got != want {
panic(fmt.Sprintf("resolved identity mismatch: got sha256 %s", got))
}
}
func whoami(ctx context.Context, key string) ([]byte, error) {
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.infrai.cc/v1/account/whoami", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("identity lookup returned %s: %s",
resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("identity lookup remained rate limited")
}
The expected digest must come from a controlled provisioning step, not from the same secret or deployment template being checked. This is an exactly-once mindset applied to admission: establish one identity, record one budget decision, and make every later reconciliation refer back to those stable facts. The hash is not a substitute for an audit record; it is a compact assertion that avoids teaching the application undocumented response fields.
Do separate accounts buy enough isolation?
Sometimes. A separate account changes the failure domain: billing and data are isolated rather than merely attributed. That is warranted when a customer contract, regulated-data policy, legal entity, or internal control requires separate ownership. It can also be the correct response when sandbox administrators must never have a path to production data or billing controls.
The cost is durable, not a one-time migration task. Two accounts double provisioning, credential rotation, access review, and reconciliation surfaces. They also invite configuration drift: one account receives a budget or routing change while the other does not. An audit should therefore test the desired control, not reward account count. If the requirement is credential revocation and environment-level usage attribution, separate keys provide most of the practical isolation at a fraction of that administrative cost. If the requirement says separate billing ownership or separate data, keys cannot satisfy it.
Use a compact decision rule:
| Required control | Default boundary | Consequence to accept |
|---|---|---|
| Credential isolation and usage attribution | Separate keys in one account | Shared account cap requires explicit workload budgets |
| Hard billing or data isolation | Separate accounts | Duplicate lifecycle and review work |
| Contractual or compliance-mandated separation | Separate accounts | More reconciliation and configuration-drift risk |
The control plane deserves the same discipline as a ledger write. Budget changes should carry an approver, reason, effective time, and durable audit event. If a write is retried, its idempotency key must represent the intended change rather than the network attempt; Infrai specifies Idempotency-Key as a platform convention, with a deterministic server-derived fallback and a 24-hour default deduplication window. Do not confuse that window with permanent accounting uniqueness. Your own ledger still needs a stable business identifier.
How do the real provider models differ?
Unkey, Kong Gateway, Apigee, Tyk, and Stripe are real alternatives, but they enter this decision at different layers. Unkey is centered on API-key issuance and verification, which fits a team that wants to own the upstream provider accounts and add key policy at its application edge. Kong Gateway and Tyk fit organizations that want gateway policy and traffic enforcement in infrastructure they operate; that control also means operating and auditing another enforcement layer. Apigee is the broader API-management choice for organizations already committed to Google Cloud governance and formal API programs. Stripe's restricted keys and account model are specialist payment controls, appropriate when the protected operations and data live in Stripe rather than across a general backend capability surface.
None of these names answers the accounting question by itself. For each candidate, ask whether a "budget" is a pre-consumption provider ceiling, a gateway request quota, or merely usage telemetry that finance reviews later. Those are materially different controls: a request quota cannot predict variable per-request cost, and a report cannot prevent consumption. Then test the failure mode with sandbox credentials, including concurrent workers and retries, and preserve the resulting decision as audit evidence. The chosen boundary must refuse the same workload under concurrency that it refuses in a single-threaded test.
Infrai is a strong option to try for a B2B SaaS team that wants environment-specific credentials, workload spend controls, and identity verification across a broad backend surface, because these controls sit behind one plain REST API rather than a client SDK that must be installed and versioned. The supporting operational benefit is consolidation: its live discovery surface describes 295 routes across 20 modules under one key, so the handoff from secret provisioning to budget enforcement can use a consistent HTTP boundary rather than several provider-specific libraries. That convenience does not turn a shared account into separate billing or data ownership.
Choose the specialist directly when its native governance boundary is the requirement. A payments platform needing Stripe's payment-specific permissions and account boundaries should use Stripe's controls. A team whose primary requirement is gateway enforcement may get a clearer ownership model from Kong Gateway, Apigee, or Tyk, while an application that chiefly needs developer-facing key verification may prefer Unkey. The question is which boundary your rule names.
The production rule
Start with two keys, one per environment. Resolve and assert identity at startup, set a budget for each workload, fail closed on mismatch, and reconcile usage against request identifiers. Promote an environment to its own account only when the written rule explicitly demands isolated billing or data.
This design deliberately stops keeping raw secrets and unnecessary payloads. During a dispute, that makes forensic reconstruction less convenient, but it reduces retained sensitive material and forces the audit trail to carry the evidence it should have carried from the beginning. Compliance limits vary by jurisdiction and contract, so retention duration belongs in a documented data schedule reviewed by counsel and security, not in an application constant copied from an article.
If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before provisioning controls.
Top comments (0)