Getting started
Point an unmodified OpenAI or Anthropic SDK at Perdurance and read a stored request back out again — from an empty account to a durable LLM call, in six steps.
By the end of this you will have made a request through Perdurance with a client you did not modify, and read the same request back out of storage afterwards.
You need one thing that is not on this page: an API key from a model provider. Perdurance calls the provider on your behalf with your credential, so there is no Perdurance-hosted model and nothing to try this against until you have one.
Sign in
Open the console and sign in. If nobody has registered your organisation yet, the console is also where that happens — the first account created owns the tenancy.
A tenancy is your organisation. Everything below lives inside it, and its slug is the first segment of every URL you will call.
Create a namespace
Namespaces → New namespace.
A namespace is an isolated set of provider connections, routing rules, keys and stored requests. Production and staging want different ones; so do two teams who should not read each other's transcripts.
Its slug is the second segment of every URL you will call. This guide assumes a tenancy acme
and a namespace prod.
Add a backend
Backends → New backend, then attach it to your namespace.
A backend is the connection to a provider: its address, its dialect, and your key for it.
| Kind | Speaks | For |
|---|---|---|
openai_compat | OpenAI | OpenAI itself, and the many servers that copy its API |
anthropic | Anthropic | Anthropic's Messages API |
openrouter | OpenAI | OpenRouter, including its Anthropic-family models |
The provider key you paste here is encrypted before it is stored, and it is never returned by any route — not to the console, and not to you.
Add a routing rule
Routing rules → New rule. Pattern *, pointing at the backend you just attached.
A routing rule is what turns the model name in a request body into a decision about which
backend serves it. * sends every model name straight through, which is what you want while
you are finding out whether this works; narrower patterns and a model_rewrite are how one
backend comes to answer for several names later.
Nothing routes until a rule exists. A request whose model matches no rule is refused rather than guessed at.
Issue an API key
Keys → New key, scoped to the namespace.
This is the key your code sends to Perdurance, and it is not the key Perdurance sends your
provider. It looks like sar_ab12cd34_… — a scheme, an eight-character public prefix that
identifies the key in listings and in logs, and the secret. It is shown once. Copy it now.
export PERDURANCE_KEY='sar_ab12cd34_…'
export PERDURANCE_URL='https://api.perdurance.dev/acme/prod/v1'Make a request
Your base URL is the tenancy and the namespace, then /v1. Under it the routes have your
provider's own names and take your provider's own bodies.
curl "$PERDURANCE_URL/chat/completions" \
-H "Authorization: Bearer $PERDURANCE_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-4.1",
"messages": [{ "role": "user", "content": "Say hello." }]
}'The response is the provider's, byte for byte. That is the point: the SDK above is unmodified and has never heard of Perdurance, and the only line that changed is the base URL.
Add "stream": true and you get the provider's own stream, forwarded as each chunk is stored.
Losing the connection
Nothing you have to do. The upstream call is already running server-side and will finish whether or not you are still listening, and the answer is stored when it does.
To pick it back up, send the identical body again. Inside the idempotency window — ten minutes from submission — it hashes to the record already there, so you attach to that execution rather than starting, and paying for, a second one. It has to be byte-identical: a re-serialised body with its keys in another order is a different request. Idempotency has the detail. A streaming request replays what was stored and then continues live from where the stored chunks end.
This is why an unmodified SDK gets durability for free: its own built-in retry is the resume mechanism, and it never learns a Perdurance-specific concept to use it.
Reading a request back
The synchronous routes above answer with the provider's response and no envelope, which means they do not hand you a request id — an id in the body is a field a vendor SDK would have to be taught to ignore. Find the request in the console, or list them:
curl "$PERDURANCE_URL/requests?limit=10" \
-H "Authorization: Bearer $PERDURANCE_KEY"Then fetch one by id. Without an Accept header you get the stored record as JSON:
curl "$PERDURANCE_URL/requests/$ID" \
-H "Authorization: Bearer $PERDURANCE_KEY"{
"request_id": "01J8F2ZK9QX3M4NBVWT7",
"status": "succeeded",
"dialect": "openai",
"streaming": false,
"attempts": 1,
"recoveries": 0,
"next_retry_at": null,
"response": { "id": "chatcmpl-...", "choices": [] }
}With Accept: text/event-stream the same URL replays the answer as a stream instead, from the
first chunk, however long ago it was stored:
curl "$PERDURANCE_URL/requests/$ID" \
-H "Authorization: Bearer $PERDURANCE_KEY" \
-H 'Accept: text/event-stream'Each event carries its sequence number as its SSE id. Send the last one you saw back as
Last-Event-ID and the replay starts from the one after it — which is what makes a reader that
died halfway through able to carry on rather than start over.
When you want the id up front
Submit to POST /requests instead. It stores the request, answers immediately, and runs the
execution without you:
curl "$PERDURANCE_URL/requests?dialect=openai" \
-H "Authorization: Bearer $PERDURANCE_KEY" \
-H 'Content-Type: application/json' \
-d '{ "model": "gpt-4.1", "messages": [{ "role": "user", "content": "Say hello." }] }'{ "request_id": "01J8F2ZK9QX3M4NBVWT7", "status": "pending" }The body is byte-for-byte the one the synchronous route takes — the dialect moves into a query
parameter precisely so it can be — so the same request submitted either way hashes the same and
deduplicates together. Collect it with GET /requests/{id} exactly as above.
Next
- Routing — patterns, priority, and pointing one name at another model
- Idempotency and resume — the window, and how a stream gets picked back up
- API reference — the full shape of every submission and retrieval route
- The
sarCLI — all of the above from a shell - Errors — including the
504that does not mean what it looks like
Durable LLM inference
What Perdurance does to an LLM request: stores it, runs the call server-side, persists every chunk, and hands the result back by id whenever you return.
Unstable AI inference
The six ways an LLM call fails that have nothing to do with the model, what each one costs, and which of them a durable proxy removes.

