# 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. Source: https://docs.stg.perdurance.dev/getting-started/ --- 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](https://console.stg.perdurance.dev) 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. ```bash 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. ```bash 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." }] }' ``` ```python from openai import OpenAI client = OpenAI( api_key=os.environ["PERDURANCE_KEY"], base_url=os.environ["PERDURANCE_URL"], ) answer = client.chat.completions.create( model="gpt-4.1", messages=[{"role": "user", "content": "Say hello."}], ) ``` ```python from anthropic import Anthropic client = Anthropic( api_key=os.environ["PERDURANCE_KEY"], base_url=os.environ["PERDURANCE_URL"], ) answer = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=64, 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](/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: ```bash 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: ```bash curl "$PERDURANCE_URL/requests/$ID" \ -H "Authorization: Bearer $PERDURANCE_KEY" ``` ```json { "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: ```bash 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: ```bash 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." }] }' ``` ```json { "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](/routing) — patterns, priority, and pointing one name at another model - [Idempotency and resume](/idempotency) — the window, and how a stream gets picked back up - [API reference](/api-reference) — the full shape of every submission and retrieval route - [The `sar` CLI](/cli) — all of the above from a shell - [Errors](/errors) — including the `504` that does not mean what it looks like