Quickstart
These docs describe the API as specified, and the wire handling, metering,
eligibility filtering, receipt chain and ledger behind it are real code you can
read and run locally. What does not exist is a deployment:
api.prismux.com does not serve and there is no key for us to issue
you. The router itself does authenticate — started without a credential register
it refuses to boot, and it rejects an unrecognised key with
401 — but no vendor credential has ever been used, so the only
completions anyone has seen from it are synthetic. Read this as a contract we are
building to, and check what is actually
running before you depend on any behaviour here.
One line. Then read the rest of this page whenever you want more control.
Point the base URL back at the vendor and you are gone, with no migration and no export to schedule — and if you had the vault on, your history is already in your own bucket. We compete on that basis rather than on a lock-in you cannot reverse.
Official SDKs
The Anthropic surface is complete under one prefix, because an SDK builds every
path it will ever call from the single base URL you hand it. Point that base URL at
/anthropic and the client you already have keeps working — there is no
Prismux wrapper to install, no fork to track, and nothing in your code that knows
our name except one string.
# python — the official anthropic package, unmodified client = anthropic.Anthropic(base_url="https://api.prismux.com/anthropic", api_key=PRISM_TOKEN) // typescript — @anthropic-ai/sdk, unmodified const client = new Anthropic({ baseURL: "https://api.prismux.com/anthropic", apiKey: PRISM_TOKEN }); # the agent SDK reads the same variable every Anthropic tool reads ANTHROPIC_BASE_URL=https://api.prismux.com/anthropic
Three paths make that work, and all three are served:
POST /anthropic/v1/messages,
GET /anthropic/v1/models, and
POST /anthropic/v1/messages/count_tokens. The last two exist because an
SDK calls them on its own: client.messages.count_tokens() against a
router that served only /v1/messages returned 404, and a
client that cannot list models cannot tell a typo from an outage.
Three official clients were run against a locally started router with
--mock-upstream, and each one completed a request, listed models and
counted tokens without a line of Prismux-specific code:
anthropic 1.3.0 for Python, @anthropic-ai/sdk 0.123.0,
and @anthropic-ai/claude-agent-sdk 0.3.260, whose turn arrived in the
receipt chain as an ordinary priced request like any other.
The completions were synthetic. No vendor credential has ever been used here, so what this measures is the wire contract and the receipt, not a model's answer. Read what is actually running before you depend on anything else.
Every response carries summary headers, and a summary is a number we assert
with nothing you can check it against. So a non-streaming response also carries
x-prism-receipt: base64 of the exact line appended to the receipt
chain — not a second rendering of the same facts, the bytes themselves. Decode it
and you hold the preimage of self_hash, which is the only property
that makes sending it worth anything.
raw = base64.b64decode(response.headers["x-prism-receipt"])
# or, without writing any code at all:
$ prism verify --receipt -
Measured on 2026-09-04 through the official Python client: the header decoded to 2,299 bytes and compared byte-for-byte equal to the line in the router's own chain file. Through the TypeScript client, 2,305 bytes, equal on the same test.
On a stream the response head is written the moment the first content byte
arrives from upstream — before the ladder has finished, before the meter has
reconciled, before the receipt exists at all. There is no receipt to put in a
header at the time the headers are sent, and a header that pretended otherwise
would be the worst kind of wrong on this site. The same bytes arrive instead as
the terminal event: prism.receipt. Two channels, the same record, at
the two different moments each response shape allows.
One more thing an SDK gives you for free: request-id is set to the
receipt id, so response.request_id — the field the Anthropic clients
already surface on every success and every raised error — is the identifier our
chain is keyed on. Minting a second id beside it is how a support conversation ends
up unable to join your log line to our record.
Model identifiers
Ask for a model, not an endpoint. claude-opus-5
resolves to whichever eligible endpoint your policy and our health data select —
that is the entire point of the router. The receipt tells you which one served
you.
If you need a specific substrate, pin it:
# any eligible endpoint (recommended) "model": "claude-opus-5" # a specific substrate and region — disables rung-0 failover "model": "claude-opus-5@bedrock/eu-central-1" # a vendor's own identifier, passed through unchanged "model": "vertex/publishers/anthropic/models/claude-opus-5"
Pinning is a real trade: you gain determinism and lose the capacity of every other quota pool. Your effective availability for a pinned model cannot exceed that single endpoint's.
The full list, with posture, jurisdiction, reasoning grade and live rates, is the catalogue.
Local & self-hosted runtimes
Ollama, llama.cpp's server, LM Studio and vLLM are ordinary
catalogue endpoints, not a footnote: same wire, same eligibility filter, same
signed receipt per request. All four publish an OpenAI-compatible
/chat/completions surface, which is why nothing in the router builds a
special request for them — a fork for a wire format we already speak would be one
more place for a body to drift.
| Model id | Enable with | Default base | Pin a weight with |
|---|---|---|---|
local-ollama | OLLAMA_BASE_URL | http://127.0.0.1:11434/v1 | OLLAMA_MODEL |
local-llamacpp | LLAMACPP_BASE_URL | http://127.0.0.1:8080/v1 | LLAMACPP_MODEL |
local-lmstudio | LMSTUDIO_BASE_URL | http://127.0.0.1:1234/v1 | LMSTUDIO_MODEL |
local-vllm | VLLM_BASE_URL | http://127.0.0.1:8000/v1 | VLLM_MODEL |
This table is generated from SPECS in
router/src/local.zig when this page is built, so a runtime added to the
router appears here or the build fails.
The thing you set is a base URL, not a key. All four ship with
authentication off, so there is no credential to issue and none to leak; where you
launched one behind --api-key, the matching
<RUNTIME>_API_KEY carries it and a bearer is sent. A router with
a local runtime configured and no vendor account at all starts and serves.
Local inference produces no vendor invoice, so all four rate cards read
0 for input and output and the receipt's provider cost is a computed
zero. That is deliberately not the same as the ten endpoints this
catalogue lists as unpriced, which
means we could not read a price. Here the price is known exactly.
An environment variable pointed at somebody else's GPU would turn an endpoint
whose whole claim is the bytes never leave this machine into a remote one
while the receipt went on saying LOCAL. So the base URL is checked
before anything is sent, and only numeric loopback literals are accepted —
127.0.0.0/8 and ::1. Even localhost is
refused, though every runtime's own README prints it, because a name is resolved
by the host and a claim we cannot check from the string alone is a claim we
should not make.
Measured on 2026-09-04, starting the shipped router with
OLLAMA_BASE_URL=http://localhost:11434/v1: it exited 78
without opening a socket, saying a local runtime's base URL must use the literal
127.0.0.1 rather than the name localhost.
retainingAll four rows declare the weakest retention posture, retaining,
marked unverified — the same posture as everything else in this
catalogue. A model on your own machine plainly has the better retention story and
we may not say so: any posture above retaining is gated on a document
somebody has read, and what a local runtime writes to its own disk — Ollama's
history, LM Studio's chat log, a llama.cpp prompt cache — is a property of
software we did not audit. The loopback check is a strong claim about where the
bytes go. It is not a claim about what happens to them when they arrive,
and conflating the two is how a compliance statement becomes false.
There is no vendor catalogue of weights to transcribe, so the catalogue carries one endpoint per runtime and the concrete weight comes from the pin above, or from a live listing if you did not pin one. If you pulled several and pinned none, the request is refused and the candidates are named. Picking the alphabetically first of four would be a silent substitution that no receipt could ever expose — it would name a real model that really answered, and nobody would think to ask why it was that one.
Declared context window with nothing configured:
4,096 tokens, which is what all four serve unconfigured. Raise it
with <RUNTIME>_MAX_CONTEXT to match how you launched the server.
Declaring a larger window here would admit a prompt the runtime then silently
truncates, which is the one failure a receipt cannot show.
Policy headers
The policy envelope travels on the request as plain headers, so it works through SDKs you did not write and tools you cannot modify. Anything you omit falls back to the key's policy, then the project's, then the organisation's — most specific wins.
| Header | Values | Default | Effect |
|---|---|---|---|
| X-Prism-Retention-Floor | zdr_contractual · zdr_configured · transient · any | key policy | Endpoints weaker than the floor are removed from the candidate set. If none remain the request fails closed. |
| X-Prism-Residency | eu · us · apac · region code | org default | Only endpoints in the named jurisdiction are eligible. Enforced, not preferred. |
| X-Prism-Max-Rung | none · substrate · version · equivalence | substrate | How far a fallback may travel. none disables failover entirely. |
| X-Prism-Pricing-Mode | price_locked · cost_transparent | price_locked | What a fallback does to your price. See pricing modes. |
| X-Prism-Project | any project id or external ref | key's project | Attributes cost to a project. Agencies use this for per-client billing. |
| X-Prism-Vault | off · managed · customer_bucket | key policy | Whether this request is sealed into your vault. |
| X-Prism-Capture-Reasoning | true · false | true | Capture reasoning text where the provider emits any. Counts are always recorded. |
| X-Prism-Consent-Downgrade | true | absent | Explicitly permits a retention downgrade for this one request. Every use is counted and appears on the receipt. |
| X-Prism-Idempotency-Key | any unique string | — | A retry with the same key returns the original receipt rather than billing twice. |
A request header can make a policy stricter than the key's policy.
It cannot make it weaker — a key pinned to zdr_contractual cannot
be talked down to transient by a header, because then the policy
would only be as strong as the least careful line of code in your codebase.
The single exception is X-Prism-Consent-Downgrade, which is
itself gated by a per-key setting that is off by default, and every use of it
is counted where you and your security team can see it.
Response headers
Every response — success or refusal — carries the provenance you need to
reconcile it later, without parsing the body. This is a real capture, pasted:
curl -D against the shipped router on 2026-09-04, started with
--mock-upstream. The x-prism-mock: true line is left in,
because a capture with its own honesty marker edited out is no longer a capture.
request-id: rcp_01M1NVNAXV72AQE4JTZRCCRVQG x-prism-receipt-id: rcp_01M1NVNAXV72AQE4JTZRCCRVQG x-prism-served-endpoint: ep_mock_anthropic_direct_use1_sonnet5 x-prism-served-model: claude-sonnet-5 x-prism-retention: retaining x-prism-jurisdiction: US x-prism-attempts: 1 x-prism-rate-card: rc_f679c826b60eb8ac8b6ea9452fca6391 x-prism-tokens-in: 2 x-prism-tokens-cache-read: 0 x-prism-tokens-cache-write: 0 x-prism-tokens-out: 92 x-prism-tokens-reasoning: 0 x-prism-tokens-reported-by: prism x-prism-upstream-tokens-out: 89 x-prism-tokens-variance: 3 x-prism-tokens-variance-out-of-band: false x-prism-provider-cost-usd: 0.000924 x-prism-charged-usd: 0.001141 x-prism-vault: off x-prism-chain-prev: prf1:genesis x-prism-mock: true x-prism-receipt: eyJyZWNlaXB0X2lkIjoicmNwXzAxTTFOVk5BWFY3MkFR…
Three more are sent when the situation calls for them, and this request was too ordinary to produce any of them:
x-prism-rung # only when a fallback was taken — how far it travelled
x-prism-reason # only when a fallback was taken — the machine reason it moved
x-prism-budget-alert # only on a request that crossed a SOFT budget ceiling and was served anyway — a hard ceiling refuses instead. It carries soft;period=;limit_usd=;committed_usd=;this_request_max_usd=;period_ends_at= — the running total and this request's own conservative maximum, because either number alone misleads — so an advisory breach reaches the caller on their own traffic rather than on an invoice four weeks later
x-prism-receipt-omitted # instead of x-prism-receipt, if a receipt ever exceeds the 8 KB header ceiling. It names the byte count rather than going quiet
The names above are checked against
responseHeaders() in router/src/main.zig when this page is
built, in both directions: a header the router gained and this page never mentioned
fails the build, and so does one this page still lists after the router dropped it.
Two numbers here are ours and say so.
x-prism-tokens-reported-by reads prism because the five
token counts are an in-path structural count — what we bill, and therefore what we
must publish. x-prism-upstream-tokens-out is what the substrate's own
usage block said, and x-prism-tokens-variance is the gap. In the capture
above they differ by 3 tokens. Nobody else publishes that number, because publishing
it means admitting the two figures can differ at all; when the substrate sent no
usage block the value is the word absent, never a zero.
On a streamed response these arrive in the response head, not as
trailers — measured on 2026-09-04, 20 of them. x-prism-receipt is not
among them and cannot be: see Official SDKs for why, and for
the terminal prism.receipt event that carries the same bytes instead.
Streaming
Both wire formats stream natively and we do not buffer. Two additions worth knowing about, both of which existing clients ignore safely:
event: prism.failover— emitted mid-stream if we move to another endpoint. Carries the reason code and the new endpoint, so your UI can say what happened instead of showing an unexplained seam. Clients that don't know the event type skip it.event: prism.receipt— the final event, carrying the full receipt.
We count tokens in the stream ourselves rather than waiting for the upstream
usage block, which may never arrive if the client disconnects. When
upstream usage does arrive we reconcile the two, and any divergence beyond
tolerance increments metering_variance on your dashboard. It is the
one number nobody else publishes because nobody else admits the two figures can
differ.
If you disconnect mid-stream, you are billed for what was actually generated up to that point — we know, because we relayed it.
Error reference
Closed taxonomy. Every error carries a machine code, one plain sentence, the constraint that bound, a receipt id, and a specific next action. A refusal here is more informative than a success elsewhere.
{
"type": "error",
"error": {
"code": "retention_floor_unmet",
"message": "This project requires zdr_contractual.
gpt-5.x is available to us only at retaining, so the request
was refused rather than sent — your prompt did not leave our
network.",
"binding_constraint": "min_retention_posture=zdr_contractual",
"candidates_considered": 2,
"candidates_eligible": 0,
"next_action": "No endpoint we hold meets zdr_contractual.
Every posture we can evidence today is retaining, so this
floor cannot be satisfied by any model here until a
retention agreement is in place and verified.",
"receipt_id": "rcp_01J8QK5T7GKC1M9PXQ2NDA4JS6",
"docs": "https://prismux.com/docs#errors"
}
}
| Code | HTTP | Means | What to do |
|---|---|---|---|
| retention_floor_unmet | 503 | No endpoint at or above your posture floor could serve this. | Choose a model with the posture, or lower the floor deliberately. |
| residency_unavailable | 503 | No endpoint in the required jurisdiction is eligible. | Widen regions_allowed, or pick a model served in-region. |
| capability_missing | 400 | Your request uses something no eligible endpoint supports. | The message names the capability. Drop it or raise max_rung. |
| reasoning_state_bound | 503 | The conversation carries reasoning state that cannot move endpoints, and that endpoint is unavailable. | Retry, or start a fresh turn without prior reasoning blocks. |
| context_length_exceeded | 400 | Input exceeds every eligible endpoint's window. | The message names the largest window available to you. |
| budget_exhausted | 402 | Your organisation’s hard spend ceiling for the period is committed — settled charges plus the holds on requests still in flight. The message names all three figures. | Raise the cap, or wait for the period to roll: the exact instant is in the message and in binding_constraint. A ceiling set as soft never refuses — it returns x-prism-budget-alert instead. |
| credit_exhausted | 402 | Prepaid balance, less outstanding reservations, will not cover the hold this request needs. The message names both numbers. | Top up. Reservations release automatically when requests settle. This refusal also carries code_alias: insufficient_credit and an x-prism-error-code-alias header — the name it shipped under before, kept so an integration pinned to it still matches. |
| rate_limited | 429 | Your own per-key limit, not an upstream one. | Retry-After is set. Upstream 429s are handled by failover, not surfaced. |
| all_endpoints_unavailable | 503 | Every eligible endpoint is failing or circuit-open. | Check status. Raising max_rung widens the set. |
| model_unknown | 404 | We do not serve that identifier. | Unknown names are refused, never quietly routed somewhere else. |
Reason codes
Every attempt records why it happened: for a fallback, how the previous attempt ended; for the first attempt, the preference that chose that endpoint over the one we would otherwise have tried — when one did. A closed enum rather than prose, because your dashboard aggregates on it and prose cannot be counted. Billable? answers one question only: were you charged for an attempt that ended this way. A move is never charged for; the attempt it opens is billed like any other.
| Code | Meaning | Billable? |
|---|---|---|
| upstream_429 | Endpoint rate-limited us. The most common trigger by a wide margin. | no |
| upstream_5xx | Endpoint returned a server error. | no |
| connect_timeout | We could not open a connection in the budget. | no |
| first_token_timeout | Connection opened, no first token within the budget. | no |
| stream_stall | Stream opened and then stopped producing. Partial output may exist. | see split delivery |
| context_length_exceeded | Input too large for that endpoint, but not for another. | no |
| content_filter | The endpoint's own filter refused. We do not retry across vendors to evade a filter. | no |
| capability_missing | Discovered at dispatch rather than admission — rare, and a bug we want reported. | no |
| quota_exhausted | Our quota on that endpoint is spent for the period. | no |
| circuit_open | The breaker for that endpoint is open; we did not try it. | no |
| cache_affinity | Not a failure: you asked for prompt caching, and we preferred the endpoint already holding your warm prefix over the one we would otherwise have tried. Recorded on the attempt the preference opened, so the decision is on your receipt rather than only in our logs. | no |
| cost_optimisation | Not a failure, and off unless you switch it on: an equally eligible endpoint — same model, same or stronger retention posture — was cheaper for us, so we used it. Available only under cost_transparent, where the whole saving is yours at an unchanged margin rate. We will not make this move under price_locked, because there the saving would be ours. With the setting off, which is the default, we never reroute for cost at all. | no |
| policy_pinned | Reserved on the attempt ladder, and reserved because the guarantee is stronger than a reason code: an endpoint pin is enforced at admission, so a pin we cannot satisfy is refused with a 503 before your prompt leaves our network, never quietly served by something the pin did not name. No attempt can carry this, because a pin removes candidates rather than causing an attempt. | no |
| manual_drain | We were draining deliberately. SIGTERM or SIGINT begins a graceful drain: the router keeps accepting — closing the listener would read as a crash to a load balancer and be retried into — and refuses new work 503 with Retry-After until in-flight requests and their open reservations settle. | no |
Split delivery
If a stream stalls after delivering tokens and we retry elsewhere, you received
output from two endpoints. That is the one genuinely awkward billing case in this
whole system, so the default is the one that costs us: absorb_retry,
where you are billed for the first delivery only and we eat the second. The
receipt marks it delivery: "split" either way.
The Prismux Receipt Format
PRF is an open specification for a signed, hash-chained, content-free record of one routed request. It is published under a permissive licence, and we would be pleased if other gateways adopted it — a format only we implement is a marketing asset, not a standard.
The three nested records
- Rate card — an immutable, content-addressed price document.
Never edited; a price change mints a new version with
supersedesset. Carries source URL, observation date, document hash and a maximum age. - Attempt — one upstream try. Endpoint, rung, the reason the previous attempt ended, retention posture, timings including first token, the five token classes, how they were counted, the rate card, provider cost, and whether it was billable.
- Receipt — the request. Policy hash, demanded capabilities,
every attempt including the ones that produced nothing, the charge breakdown, a
promisesblock recording each guarantee kept or broken, vault status, the previous receipt's hash, its own hash, and a signature.
Why it cannot contain your content
The schema admits counts, identifiers, hashes, enumerations and timestamps.
There is no free-text field, no body field and no header map. A guard walks the
schema on every change and fails the build on any unconstrained string; a second
guard walks the database columns so a future migration cannot quietly add a
prompt column. That is what makes our retention claim structural
rather than a policy we ask you to trust.
The chain
Receipts are chained per organisation: each carries the hash of the previous
one. A receipt cannot be deleted, reordered or inserted after the fact without
breaking the chain, and prism verify proves that offline. Daily chain
heads and a monthly Merkle root — which is what lets you prove
completeness rather than only integrity — are specified, and the router
carries a comment where that code will go rather than the code. So a chain you hold
is provably unaltered and provably ungapped internally, and completeness is
the part you are still taking from us.
Corrections are append-only credit_note records that reference the
original receipt, name a reason from a closed enum, and join the same chain. We
cannot quietly edit a billing error — which is the point, and also why our pricing
has to be right the first time.
Open-source verifier
A single static binary, in a different language from our data plane, with no dependency on our infrastructure. That is deliberate and it is the whole design constraint: verification that requires our SDK is not verification.
Against a four-request chain produced by a local router in mock-upstream mode
on 31 August 2026. The volumes are small because they are the volumes that exist:
an earlier version of this page showed a transcript of 412,880 receipts
reconciling against a $202,203.42 invoice, which was typed rather than run. There
are no invoices to reconcile yet, so --invoice is not shown here.
$ prism verify receipts-2026-08.jsonl --keys prismux-receipts.pub ◢◤ prismux verify reading 4 receipts from receipts-2026-08.jsonl keys 1 from prismux-receipts.pub ✓ rcp_01M1B6WPD1PVJ… claude-sonnet-5 direct $0.001218 ✓ rcp_01M1B6WPDB7HH… claude-sonnet-5 direct $0.001218 ✓ rcp_01M1B6WPDM8NW… claude-sonnet-5 direct $0.001218 ✓ rcp_01M1B6WPDY2S6… claude-sonnet-5 direct $0.001218 ──────────────────────────────────────────────────────────────────────── ✓ signatures 4 of 4 verified against the key you supplied ✓ 4 receipts · chain intact from genesis · total $0.004872 $ echo $? 0
The key came from the router's own start-up banner, which prints the line
signing <id> ed25519pub:<base64> and tells you to pin it.
prism verify reads that line verbatim.
It reads an export and a published key, recomputes every hash, walks the chain, checks for sequence gaps, and reconciles the total against your invoice. It never contacts us. Run it in your own CI against every invoice if you like — several finance teams will, and that is the intended use.
What a failure looks like
Both of these are real runs against the same four-receipt chain, one with the
charged field of record 2 edited, one with record 3 deleted.
$ prism verify receipts-tampered.jsonl --keys prismux-receipts.pub ✓ rcp_01M1B6WPD1PVJ… claude-sonnet-5 direct $0.001218 ✗ rcp_01M1B6WPDB7HH… claude-sonnet-5 direct $9.990000 ✓ rcp_01M1B6WPDM8NW… claude-sonnet-5 direct $0.001218 ✓ rcp_01M1B6WPDY2S6… claude-sonnet-5 direct $0.001218 ──────────────────────────────────────────────────────────────────────── ✗ rcp_01M1B6WPDB7HHG9… line 2 content hash mismatch — this receipt was altered after signing • signatures 4 of 4 verified — but see the hash failures above: a signature attests the hash, not the content ✗ 1 failures · an invoice built on this chain cannot be trusted · total $9.993654 intact through 1 receipts, then line 2 $ echo $? 1 $ prism verify receipts-gap.jsonl --keys prismux-receipts.pub ✗ rcp_01M1B6WPDY2S6RA… line 3 broken link — the record before this one does not match intact through 2 receipts, then line 3 $ echo $? 1
“signatures 4 of 4 verified — but see the hash failures above: a signature attests the hash, not the content.” An edited record still carries a valid signature over the hash it was signed with; what catches it is that the hash no longer matches the record. The two checks are independent and the verifier says so rather than letting a green signature line imply an intact record.
prism — the companion CLI
prism is deliberately not another coding agent. Building
one would mean competing with the tools you already use, and we would rather
work with them. It reads your receipt stream and your vault
instead, and gives whatever you already use the layer it lacks.
| Command | What it does |
|---|---|
| prism watch | Live cost as you work — per session, per task, per file touched — for any tool routing through Prismux. |
| prism sessions | Every session reconstructed from receipts and vault, across every tool you used, in one format. |
| prism export | Decrypts locally with your key; writes JSONL, SQLite, Parquet or Markdown. Content search happens here, on your machine, because it structurally cannot happen on ours. |
| prism why <receipt> | The full routing ladder for one request, verified offline against the chain. |
| prism budget | Caps, alerts and burn-rate projection from the terminal. |
| prism replay | Re-run a captured session against a different model and diff the outcome and the cost. Only possible on top of the vault-plus-receipt substrate. |
| prism verify | Wraps prism-verify for the common case. |
Limits and retries
Upstream rate limits are ours to absorb, not yours to see. A 429 from a vendor
becomes a rung-0 failover to another quota pool; you see a 429 from us only when
your own per-key limit is the binding constraint, and then
Retry-After is set honestly.
- Retry safely with
X-Prism-Idempotency-Key. A retry carrying the same key returns the original receipt rather than billing a second time. Keys are held for 24 hours. - Reservations, not post-hoc billing. A conservative maximum
is held against your balance and tested against your period ceiling at
admission — in one critical section, before anything is forwarded — and
released on settlement. So an in-flight request’s maximum is already spent as
far as every concurrent request is concerned, and N requests arriving in the same
instant cannot each decide independently that they fit. That is what makes
budget_exhausteda guarantee rather than a best effort: not that we check a number, but where we check it. - Hard ceilings refuse; soft ceilings alert. A hard ceiling
returns
budget_exhaustedbefore a token is forwarded. A soft one always serves and reports instead:x-prism-budget-alerton the response, and abudget_soft_breachescounter on/healthfor whoever is watching the account rather than the request. Periods are UTC —dailyrolls at 00:00Z,weeklyon Monday 00:00Z,monthlyon the 1st — and a ceiling is set per organisation withPOST /v1/admin/orgs/<org>/budget, amounts as quoted decimal strings so they never pass through a float. - Circuit breakers are per endpoint, with exponential probe
backoff and half-open recovery. An endpoint we have marked open is skipped
rather than tried and failed, which is why
circuit_openattempts cost nothing and add no latency.
Vault and export API
Three modes, configured per key or per request. Full explanation of the trade on Trust → the vault.
# List sealed records (metadata only — we cannot read the contents) GET /v1/vault/objects?since=2026-08-01&project=acme-client-14 # Fetch ciphertext. Unwrapping happens on your machine, with your key. GET /v1/vault/objects/rcp_01J8QK4M2XN7B3VYFH0RTZ6WC9 # Export receipts for verification and reconciliation GET /v1/receipts/export?month=2026-08&format=jsonl # Published signing keys, with validity windows and rotation history GET /.well-known/prismux-signing-keys.json
In customer_bucket mode the object endpoints
return a pointer into your own bucket rather than bytes from us, because in that
mode we hold nothing at all — not even the ciphertext.