Customer spending caps
A dollar line per one of your customers, refused before your provider is called, on Team and above.
On this page
- What it is
- One counter per customer
- The windows are UTC calendar windows
- The hard stop is a switch
- How far past a cap a customer can get
- The headers your server gets back
- A refusal is final until reset or a raise
- The tag
- The 10,000-customer bound
- What counts, and what does not
- Streaming
- Who can change a cap, and on which plan
- On Free: the count
- No mail per customer
- Send the tag, per stack
- See also
Tag a request with the customer it serves. When that customer reaches its cap the
request is refused before your provider is called; your other customers keep
going; you get a 402 that says customer.
"Customer" on this page means your customer: the people and companies who use your product. Your own account with us is your organization.
What it is
- Tag a request with the customer it serves:
x-recovea-tags: customer:<your id>. - The value is opaque. We compare it and count against it; we never resolve it to a person, never write it to one of our service log lines, and never put it in a refusal body. It does live in your own organization's records, which is how you cut your spend by it (Tags · What is done with a value). No prompt or completion is ever stored.
- A customer spending cap is a dollar line per declared customer per window, on your own provider key.
- When a customer reaches its cap the request is refused before your provider is called. Your other customers keep going.
- Every response served under an armed customer spending cap carries
x-recovea-budget-remaining: the dollars left for that customer in this window. A cap with its hard stop off prints no figure of its own. - The refusal is
402, typeinsufficient_quota, codebudget_exceeded. Branch on the code, not the status. - A bound of 10,000 distinct customer values per organization per calendar month (The 10,000-customer bound).
- Customer spending caps are Team and above. On Free your organization keeps the count of customers tagged this month and what they cost in total (On Free: the count).
One counter per customer
We price each request from your provider's published price list, add it to what that customer has spent in the current window, and compare the total with that customer's cap. The counter is keyed by the organization, the customer and the window's period and by nothing else, so raising or renaming a cap keeps the spend already recorded against that customer.
Every node that admits your traffic holds its own counter, and our control plane combines them from the priced record every five seconds; a cap you have just armed is read on the next one of those ticks. What comes back can only ever raise a node's counter, never lower it, so a node that has not heard yet refuses a little late and never early.
Two shapes exist, and together they are the whole feature:
| Shape | What it covers | Rows |
|---|---|---|
| A cap for every customer (the default rule) | every customer you tag, each in its own counter | one per window |
| A cap for one customer (an override) | that one customer | one per customer per window |
While an override is armed it replaces the default rule for that customer, on that window. A disarmed override counts and the default rule still binds, because only armed rows are read as caps. A default rule on a different window still applies. Where two default rules cover the same customer, the tightest decides: two caps on two clocks keep separate counts, and nothing in the product adds them up.
The windows are UTC calendar windows
The five windows are the same ones every cap uses: day, week, month, year, and one that never resets. Each opens and closes at 00:00 UTC (Where a cap is set).
Caps run on UTC calendar windows for every customer alike; a per-customer anniversary window is not offered. If you sell a plan that renews on the 17th, a monthly cap still resets on the 1st.
The recipe when your own renewal day is what matters. Give that customer a cap on the window that never resets, and raise it by the included dollars each time you renew them, in the platform. Spend to date is kept when a cap is raised, because the counter is keyed by scope rather than by the cap's own row, and traffic resumes within about five seconds of the raise: the raise goes out on our control plane's five-second tick, and in a two-node test on a local estate on 2026-09-12 the gateway applied it 9 to 24 milliseconds after that tick.
A cap that never resets sends no x-recovea-budget-resets-at, not on a served
response and not on a refusal, so what clears your own cache for that customer is
your raise, not a time.
The hard stop is a switch
A cap with its hard stop off counts and refuses nothing. A cap with its hard stop on refuses. Turning one on is one toggle in the platform, and a cap with no hard stop is the honest way to watch a number before you enforce it (The cap form).
Customer spending caps stop; they do not pause. A paused cap would answer
429 with Retry-After, and every official SDK retries a 429, which would turn
one refused customer into a retry loop labeled as a rate limit. A customer cap
created in pause mode is refused with "A customer spending cap stops; it does
not pause."
How far past a cap a customer can get
A cap stops the next request, not the ones already running. The request that
crosses the line is admitted and priced when it finishes, and requests running at
the same moment finish too. So a customer's total can end above its cap by about
the cost of the requests in flight when it was reached: for one large request on
an expensive model, several dollars. If you need a hard stop at a number, set the
cap below it by the cost of your largest request, or read
x-recovea-budget-remaining and stop earlier in your own code.
Before your provider is called, the comparison adds a floor for this request to what that customer has already spent, and refuses if the two reach the cap. The floor counts the size of the request body in bytes divided by four, at the lowest rate the gateway's price list charges for any input token, cached or not, rounded down. It can be more than the request costs, most of all for images, audio and files, which count every byte of their encoding, so a cap can refuse a request whose priced cost would have fit. When the response ends, the price the gateway gives it replaces the floor.
A request the gateway cannot price, including one the provider refused or never answered, counts toward the cap while it runs, and when it ends its floor is released on the node that admitted it. So a cap does not bound spend the gateway cannot price, such as spend on a model its price list does not carry (What counts). In two cases a floor outlasts its request. When the gateway's nodes exchanged the counter while the request ran, the floor can stay on the counter until the cap's window ends, since an exchange only ever raises a counter. While a send from the gateway to its metering queue is slow, the floors of requests that have already ended stay on the node that admitted them, and a cap can refuse a new request on those floors. Once the send returns they are released, or replaced by the requests' prices; the gateway sets no time limit on that wait. A request whose client left after it was sent, including a stream abandoned midway, has its floor released too, though your provider may bill the call.
The headers your server gets back
Five header values carry the cap's state, and they ride in two tiers.
The pair, x-recovea-budget-scope and x-recovea-budget-window, rides every
cap refusal and every response served under an armed cap of any kind, yours or
one of your customers'. On a served response it names the tightest armed cap the
request is bound by, which is the same cap a refusal would name. Where the three
figures ride, it names the customer's own cap instead: one response describes one
cap, because organization printed beside a customer's remaining dollars would
read as your whole account having that much left.
The three figures, -remaining, -cap and -resets-at, ride only a response
served under an armed customer spending cap. A cap that is only being watched
counts and prints nothing.
| Header | Served under an armed customer cap | On a customer refusal | Served under an armed cap of another kind |
|---|---|---|---|
x-recovea-budget-scope |
customer |
customer |
organization · key · run |
x-recovea-budget-window |
that cap's window | the same | that cap's window |
x-recovea-budget-remaining |
dollars left, six decimals | absent | absent |
x-recovea-budget-cap |
the cap, six decimals | absent | absent |
x-recovea-budget-resets-at |
the next boundary, UTC; absent on a cap that never resets | present, except on a cap that never resets | absent |
A refusal from a cap of another kind carries the pair and nothing else. The reset instant rides a customer refusal alone, because the builder who set their own cap already knows its window and your customer's app does not.
Nothing at all rides when no armed cap stood over the request (no cap of
yours and none of your customers', or a cap whose hard stop is off), and nothing
rides a refusal that read no cap figure: a 401, a throttle, the 400 for a
malformed tag, the overflow 402.
What remaining means. It is counted after this request is admitted and
before any request still running for the same customer is priced; treat it as
the most that is left, never the least. 0.000000 on a successful response
means the next request for this customer is refused, because the entry quoted is
armed and the request in front of it already cleared the comparison. It is also
rare: the figure usually reaches zero through pricing, a peer node, or a cap you
lowered, all after a response has been served. The number to act on is a
small remaining figure, not a zero one. The share used is
1 − remaining/cap.
These headers are for your server; do not forward them to a browser or a customer-facing client.
Read the code and the scope together. budget_exceeded with
x-recovea-budget-scope: customer is that customer's included usage ending; any
other scope is a cap of your own. budget_unverifiable is a temporary condition
on our side and does not arrive on a 402 at all: it answers 503 with
Retry-After: 1, which the official OpenAI and Anthropic SDKs retry by default,
and it never deserves a quota message
(Fail-open and fail-closed).
Headroom on a refusal would read as permission, which is why remaining and
cap are absent there and the reset instant is not: a time is the closing
sentence in machine form.
A refusal is final until reset or a raise
A 402 is final until reset or a raise; official SDKs do not retry it and you
should not. Every retry is refused before your provider is called, still counts
against your key's request rate, and is recorded as a refused request.
The first few seconds after a refusal. A refusal can be followed by a served request while the nodes converge. In a two-node test on a local estate on 2026-09-12, one node refused between 0.7 and 5.0 seconds after the other did, about 2.8 seconds at the median over twenty runs: the window is one checkpoint tick, and the crossing lands wherever it lands inside one. Rarely it is longer: when both nodes publish in the same instant it can take well over a minute. Every one of those waits is in the safe direction: the node that has not heard yet keeps serving, so it refuses late and never early.
Treat the first budget_exceeded for a customer as their state until
x-recovea-budget-resets-at or until you raise the cap. The engine holds no
sticky latch on purpose: a latch would keep refusing a customer whose cap you had
just legitimately raised.
The recipe. On the first 402 for a customer, store
customer → x-recovea-budget-resets-at in your own cache and answer that customer
from the cache until the instant passes or you raise their cap. Your own
short-circuit is faster than ours, it is yours to clear, and it is the only way
not to see the window at all.
The tag
- Read as bytes, then decoded as UTF-8 piece by piece, so a value like
customer:zoëcounts as itself. - Every
x-recovea-tagsheader line is read, and each is split on commas first. - The first
customer:piece decides. Send onecustomer:tag per request; if you send more, the first one is the customer the cap holds. - 1 to 128 characters, any UTF-8. No commas in the value, because the header splits on them. Case and spaces count. Keep your ids unique in their first 128 characters.
- Never truncated. A value cut short is a different customer: two ids sharing a 128-character prefix would collapse into one counter and the second customer would be refused on the first one's spend.
- On an organization holding an armed customer spending cap, a value that is
empty, over-long, or not valid UTF-8 is refused
400invalid_customer_tagwith the sentence "The customer tag must be 1 to 128 characters." and no echo of the value. On an organization with no armed customer cap the value is dropped and the request is counted with no customer. - The tag is optional. A request with no
customer:tag counts against your organization's cap and the key's cap only, and no customer rule can refuse it. - The header never reaches your provider. It is ours, and it is removed before the request is forwarded.
The full header (the four namespaces, the limits, what is removed) is Tags.
The 10,000-customer bound
10,000 distinct customer values per organization per calendar month, whatever window your cap runs on. It is our engineering bound, not a statement about your money, and it is not a number on a pricing card.
The count is kept across our gateway's nodes: each node shares the customers it has newly seen with the others within about five seconds. So new customers that arrive on different nodes in the same few seconds can take an organization slightly past 10,000 before the bound applies.
Above it, on an organization holding an armed customer spending cap, a customer
we have not seen this month is refused with its own code,
customer_cardinality_exceeded
(the wire), and
nothing is charged. A customer that already holds a cap of its own is admitted
past the bound, and counts toward the 10,000 once it has sent traffic this
month; one that has sent none this month is not counted. With no armed customer
cap, nothing is refused and the tag rides through to the record as it always
did.
Counters for a closed period are reclaimed after a grace period; a live period's counter never is.
What counts, and what does not
- Costs are priced from your provider's published list prices, cached and uncached tokens alike, over the token counts your provider reported.
- A request on a model that is not on the list is logged and counted as a
request, and counts toward the cap only at its floor, never at a priced
figure. So does a response we could not price at all: a stream the client
abandoned, or a reply the provider returned no token counts for. Each counts
while it runs, and when it ends its floor is released, apart from two cases
(How far past a cap), so a cap does
not bound that spend. One the provider never answered, or whose client left
before the answer began, leaves no record. The per-customer figures report the
unpriced requests they record separately as
unpricedRequestsand never fold one into that row's spend total, which is a different number from the counter your cap runs on. - The price list that priced the period is named beside the figures. When a month spans two lists, no list is named rather than one of them being named for all.
- Your provider's invoice is your provider's. Ours is the same traffic priced at the published list, and the two are close, not identical.
Streaming
A streamed reply carries the header values with its reply headers, before the first chunk: they are written onto the response the moment the stream is handed back. A refusal is always a whole request and never a cut in the middle of an answer: there is no mid-stream re-check, and the request is metered once, after the last byte.
Who can change a cap, and on which plan
- Only owners and admins can set, change, arm, disarm or delete a customer
spending cap; Members and Viewers see every one; a Billing role sees none. A
Member who tries is refused
403with "Only owners and admins can change customer spending caps." - Customer spending caps are Team and above. Caps on your organization and on your API keys keep working on every plan.
- If your plan drops below Team, an armed cap keeps refusing: the enforcing node reads no plan at all. You can still lower it, rename it, move it to another window, turn its hard stop off, or delete it, from the platform. Adding a new one, raising an existing one, and turning a hard stop back on are what need the plan. So turning one off below Team is a one-way door until you are back on it: leave it armed and lower it instead if you mean to keep the stop.
- The remaining-line header returns to your server on the tagged response. Your customer sees it only if your code forwards it.
The role table and the two fences are in Where a cap is set.
On Free: the count
Every plan, Free included, can see how many customers were tagged this month and what they cost in total. There is no per-customer row in that answer, and no field for one exists in the response. A cap per customer, and the per-customer figures, are Team and above.
No mail per customer
We never email you when one of your customers is refused, and we never contact
your customers at all. A default rule over five thousand customers would
otherwise mail you thousands of times. You see refusals in the platform, each
refusal is kept in the record, and your own code sees the 402 the moment it
happens. Alert percentages saved on a customer cap send no mail; the threshold
alerts on Team and above are for your organization's cap and your keys' caps
(Alert thresholds).
Send the tag, per stack
One extra header per request. Per call, never a client-wide default: the customer changes from one request to the next, and a header set once at construction time would count every customer as the first one.
curl
curl https://api.recovea.ai/v1/chat/completions \
-H "Authorization: Bearer $RECOVEA_KEY" \
-H "Content-Type: application/json" \
-H "x-recovea-tags: customer:8812" \
-d '{"model":"gpt-5-nano","messages":[{"role":"user","content":"hello"}]}'OpenAI, Python
client.chat.completions.create(
model=model,
messages=messages,
extra_headers={"x-recovea-tags": f"customer:{customer_id}"},
)OpenAI, TypeScript
await client.chat.completions.create(
{ model, messages },
{ headers: { 'x-recovea-tags': `customer:${customerId}` } },
)Anthropic, Python
client.messages.create(
model=model,
max_tokens=1024,
messages=messages,
extra_headers={"x-recovea-tags": f"customer:{customer_id}"},
)Anthropic, TypeScript
await client.messages.create(
{ model, max_tokens: 1024, messages },
{ headers: { 'x-recovea-tags': `customer:${customerId}` } },
)Vercel AI SDK
const result = await generateText({
model: openai('gpt-5-nano'),
prompt,
headers: { 'x-recovea-tags': `customer:${customerId}` },
})The one thing every snippet has in common: the header is on the call, beside
the messages, not on the client. A client built once with
defaultHeaders: { 'x-recovea-tags': 'customer:8812' } bills every customer you
have to customer 8812.
See also
- Set up spending caps for your customers: set it up, top to bottom, in about ten minutes.
- Customer spending caps: the wire: the header, the five response values, the refusals, the shapes.
- Customer spending caps: troubleshooting: one entry per question, with its cause and its fix.
- Tags: the
x-recovea-tagsheader itself. - Where a cap is set: where every cap is set, who may change which, and the five windows.
- Breaker: a budget one run carries, on every plan.