Guide

Multi-tenant AI billing: why per-customer API keys don't work

A provider key authenticates you to the model vendor, not a customer to you. Treating it as a tenancy boundary is the default mistake, and it's an easy one because nothing in a demo catches it.

The instinct, and where it breaks

The first idea, every time: give each customer their own provider API key. It sounds like isolation: different key, different customer, different bill. It holds for exactly as long as you have one customer to test it against.

Rate limits are set per key, not per tenant’s workload, so one customer running a batch job exhausts the shared ceiling and every other tenant on that key starts failing with them. Provision one real key per customer instead and you’ve traded that problem for another: N keys to rotate, N keys to revoke on offboarding, N secrets now living in your admin panel, your logs, and whatever support tooling needed to debug customer 40’s account. Key sprawl grows linearly with customers, which means it becomes an operations line item exactly when you can least afford one.

A key conflates three things that have to stay separate

A bearer credential answers one question: is this caller allowed to make this call. Tenancy needs three different answers, and a provider key was never built to give more than the first:

ConcernQuestion it answersWhat a provider key gives you
AuthenticationIs this caller who it claims to be?Yes. This is what the key is actually for.
MeteringWhat did this cost, attributable to whom?Only if you built a side table mapping keys back to customers.
IsolationCan tenant A’s data ever reach tenant B?No. The key governs the call to the model vendor, not your own retrieval index, cache, or logs.

That third row is the one that costs teams a security review. Revoking a leaked key stops new calls to the model provider. It does nothing about the customer’s data already sitting in a shared vector index, a shared cache, or a shared trace store. The key was never the boundary for any of those, only for the one call it happened to authenticate.

Where the boundary actually has to live

Isolation has to be enforced where the data lives, not where the model gets called: on every row, every cache key, every queued job, every trace, tagged with a tenant identifier that has nothing to do with which provider credential served the request. The specification calls this identifier payer_id, requires it to be opaque, and attaches it to every cost-bearing record rather than to whichever key happened to be in the request headers:

POST /v1/turns
{ "tenant": "ten_04qf",
  "message": "Summarize this thread" }

One platform credential authenticates your service. The tenant is a field in the record, resolved from your own request context and carried through every component the turn touches. Revoking a customer becomes a query against rows tagged with their payer_id, not a support ticket filed with a model vendor hoping a key revocation propagates before the next billing cycle closes.

What this buys you that a key never could

Decoupling tenancy from the provider credential means you can rotate keys, add a failover provider, or switch model vendors entirely without touching a single tenant boundary. The boundary was never attached to the credential to begin with. It also means pricing tiers are a product decision instead of a side effect of which key a customer happened to be issued: the specification’s class_basisfield lets a turn’s class follow the model, the shape of the work, or what the customer selected, declared explicitly rather than implied by infrastructure.

Opaque payer attribution and the no-orphan rule are defined in turn-accounting §3.4, Attribution and orphans and §5.1.