# API reference > The submission and retrieval routes for durable LLM inference, SSE replay with Last-Event-ID, and the record a request leaves behind. Source: https://docs.stg.perdurance.dev/api-reference/ --- Every route below is scoped to a tenancy and a namespace: ``` https://api.perdurance.dev/{tenancy}/{namespace}/v1 ``` and authenticated with an API key issued for that namespace: ```http Authorization: Bearer sar_ab12cd34_... ``` A key is `sar__`. The eight-character prefix is the public half — it is what listings and log lines name a key by, and the secret is stored only as a hash. The administration routes the console drives — namespaces, backends, routing rules, users and keys — are in [Administration](/administration). Every status this API can return is in [Errors](/errors). ## Submission | Route | Dialect | Answers | |---|---|---| | `POST /chat/completions` | OpenAI | The connection is held: a completed response, or SSE of `chat.completion.chunk` when the body says `"stream": true` | | `POST /messages` | Anthropic | The connection is held: a completed message, or SSE of Anthropic's own events when streaming | | `POST /requests?dialect=` | Named by the parameter | `202` with the request id, immediately | The request body is forwarded to the provider byte for byte, except the model id a routing rule rewrites. The answer comes back exactly as the provider sent it, with no envelope — which is what lets an unmodified vendor SDK work against these routes. ### Dialect is decided by the route, never sniffed `dialect` on `POST /requests` takes `openai` or `anthropic`, and defaults to `openai`. Nothing inspects the body to guess. When a routing rule resolves a request to a backend of the *other* dialect it is refused with `422` and no record is written. Perdurance does not translate between dialects: a translation layer owns a mapping it has to keep current with two vendors, and every field it fails to understand is a corrupted upstream call. Reaching an Anthropic model from an OpenAI client is what an `openai_compat` or `openrouter` backend is for — see [Routing](/routing). ### `202` from `POST /requests` ```json { "request_id": "01J8F2ZK9QX3M4NBVWT7", "status": "pending" } ``` ### Re-sending an identical body Inside the idempotency window — ten minutes — a byte-identical body hashes onto the record already there. [Idempotency and resume](/idempotency) covers what counts as identical. | Record's state when the hash hits | Not streaming | Streaming | |---|---|---| | `running` | Holds until it settles, then returns the stored answer | Replays the stored chunks, then live-tails to completion | | `succeeded`, `failed` | Returns what was stored | Replays the stored chunks | The provider is called once across all of it. ## Retrieval ### `GET /requests/{id}` Without `Accept: text/event-stream`, one JSON object: ```json { "request_id": "01J8F2ZK9QX3M4NBVWT7", "status": "succeeded", "dialect": "openai", "streaming": false, "attempts": 1, "recoveries": 0, "next_retry_at": null, "response": {} } ``` `response` carries the stored answer, embedded exactly as it was stored rather than reserialised — a response whose keys moved is no longer the provider's own bytes. A failed request carries `error` instead. A request that has not finished carries neither, which is what makes "no answer yet" a fact about the shape rather than something you parse for. A **streamed** answer lives in its chunks and appears under neither field. Read it through the replay below. `attempts`, `recoveries` and `next_retry_at` are the execution history: how many times the request was taken up, how many of those followed a worker dying mid-flight, and when the next attempt is due. ### `GET /requests/{id}` with `Accept: text/event-stream` The same URL replays the answer as SSE. Each event carries its sequence number as its `id`. | Request state | Behaviour | |---|---| | `pending` | Waits for the first chunk, or returns the terminal state if execution fails first | | `running` | Replays the stored chunks, then live-tails | | `succeeded`, `failed` | Replays the stored chunks, then ends | Chunks are replayed as the provider framed them, event names included, with `id: ` written in front. Strip the `id` lines and you have exactly what the provider sent. A request that failed ends its replay with an `event: failed` carrying the stored error. It has no `id`, because a failure is not a place to resume from. ### Resuming ```http Last-Event-ID: 25 ``` The reply starts at sequence 26 and otherwise behaves exactly as above, live tail included. A `Last-Event-ID` past the end simply waits for what has not arrived yet. ### `GET /requests` A listing, newest first, for finding an id you did not keep. | Parameter | Meaning | |---|---| | `limit` | How many to return | | `after` | An opaque cursor from a previous page — hand back what you were given | Each row carries the request's status, model, dialect, key and backend, its token counts, how many bytes it holds, and its created, started and finished timestamps. The stored body and answer are deliberately absent: they are the largest columns in the table and a page of them would be megabytes. `GET /requests/{id}` is where a body is read, one at a time.