Usage and storage
What a namespace has spent per key, what it is holding, and why accepted submissions and provider calls are different numbers.
Two routes, both scoped to a namespace and both needing a key that can manage it.
GET /usage
What the namespace has spent, in total and attributed per key.
curl "$PERDURANCE_URL/usage" -H "Authorization: Bearer $PERDURANCE_KEY"{
"namespace_id": "…",
"itemised_since": 1787900000000,
"accepted_submissions": 1841,
"provider_calls": 1792,
"upstream_sent_bytes": 4192048,
"upstream_received_bytes": 19402881,
"api_keys": [
{
"api_key_id": "…",
"ledger_events": 1204,
"provider_calls": 1180,
"prompt_tokens": 884102,
"completion_tokens": 219884,
"total_tokens": 1103986
}
]
}accepted_submissions and provider_calls are deliberately different numbers. A submission
that landed on an existing execution inside the idempotency window is counted once as a
submission and not at all as a provider call. The gap between them is what deduplication saved
you.
The totals above api_keys are all-time. The itemisation is not: it covers the last thirty
days by default, and itemised_since echoes back the start of the window that was used, in
epoch milliseconds, rather than the oldest event found in it. ?since=<epoch millis> widens or
narrows the window:
curl "$PERDURANCE_URL/usage?since=1780000000000" -H "Authorization: Bearer $PERDURANCE_KEY"The window is bounded by default because the itemisation aggregates the request history, which grows with the age of the namespace. The namespace-wide figures are a rollup and cost the same whatever the window is.
A token count is null where no answer under that key reported one, which is not the same as a
provider reporting zero. Treat null as "not known", not as "none".
Attribution
Every figure in api_keys is per key, which is what makes a key the unit of cost accounting.
Issue one per service, per environment, or per customer, and this route tells you what each of
them spent without a second system.
There is no per-model breakdown here. Per-model detail is on the listing: every row of
GET /requests carries the model it ran.
GET /storage
What the namespace is currently holding, and where it went.
curl "$PERDURANCE_URL/storage" -H "Authorization: Bearer $PERDURANCE_KEY"{
"namespace_id": "…",
"stored_bytes": 20418302,
"request_bytes": 4192048,
"response_bytes": 2884102,
"chunk_bytes": 13342152
}stored_bytes is the sum of the other three. The split matters because they grow for different
reasons: request_bytes tracks how much you send, response_bytes how much comes back whole,
and chunk_bytes how much comes back streamed — which is usually the largest, because a streamed
answer is stored as its individual frames so it can be replayed and resumed.
Keeping it down
Retention is a namespace setting, in minutes, and it is unset by default. Set it in the
console under Namespaces, or with PATCH /{tenancy}/v1/namespaces/{namespace}. A namespace that
names no horizon takes its tenancy's; a namespace that names 0 asks to keep everything even
where its tenancy sweeps.
There is a ceiling on that ask. A record cannot outlive the deployment's metadata horizon,
which is three months by default, because the metadata sweep deletes the request row and the
payload goes with it. The shorter of the two wins. That is a silent clamp rather than a refused
write, so 0 means "as long as the deployment keeps the metadata" rather than "for ever". You
do not have to work the rule out. PATCH /{tenancy}/v1/namespaces/{namespace} answers with
effective_retention_minutes, which is the figure the sweep will apply.
What a sweep takes is the record: the request body, the answer, and the chunks. What it leaves is everything you bill and report from — the status, the model, the token counts, the cost, the timings, and the fee ledger. Those have their own, much longer horizons, set for the whole deployment rather than per namespace.
So shortening a namespace's retention bounds your disk without costing you the ability to
reconstruct an invoice. Retrieving a swept record answers 410 Gone rather than 404 Not Found,
because the request did happen and everything about what it did is still there.
Lapsed idempotency claims are swept whatever retention says, because a claim is not a record.
Per-request sizes are also in the listing: GET /requests gives stored_bytes per row, which is
how you find the handful of requests holding most of it.
Administration API
Namespaces, backends, routing rules, members and keys — everything the console drives.
Errors and status codes
Every status the submission and retrieval routes return, the stable code that comes with it, and what to do about each — including the 504 that does not mean your request failed.

