> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.empiriolabs.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.empiriolabs.ai/_mcp/server.

# Long Responses

An HTTP request to the API can run for up to 15 minutes. For a response that takes longer, connect to `/v1/responses` over a WebSocket and send the same request. This works with every text model, and it uses the WebSocket mode of the OpenAI Responses API, so the OpenAI SDKs connect to it directly.

Nothing is stored. The response reaches you as it is written, like a stream, so stay connected until it ends.

## Endpoint

`WEBSOCKET /v1/responses`

```text
wss://api.empiriolabs.ai/v1/responses
```

Authenticate with your API key in the `Authorization` header of the WebSocket handshake:

```text
Authorization: Bearer $EMPIRIOLABS_API_KEY
```

If the connection cannot start, the handshake is refused with the same HTTP status and JSON error body as the REST API.

## Send a request

Each request is a `response.create` message: the `POST /v1/responses` body, with `type` added and without `stream`. Events come back as one JSON message each, the same events a streaming `POST /v1/responses` sends.

With the OpenAI Python SDK (`pip install "openai[realtime]"`):

```python
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["EMPIRIOLABS_API_KEY"],
    base_url="https://api.empiriolabs.ai/v1",
)

DONE = ("response.completed", "response.incomplete", "response.failed", "error")

with client.responses.connect() as connection:
    connection.response.create(
        model="kimi-k3-1-pro",
        input="Write a full specification for a task scheduler.",
        max_output_tokens=100000,
    )
    for event in connection:
        if event.type == "response.output_text.delta":
            print(event.delta, end="", flush=True)
        elif event.type in DONE:
            break
```

Without an SDK:

```python
import asyncio
import json
import os

import websockets


URL = "wss://api.empiriolabs.ai/v1/responses"
DONE = ("response.completed", "response.incomplete", "response.failed", "error")


async def main():
    headers = {"Authorization": f"Bearer {os.environ['EMPIRIOLABS_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({
            "type": "response.create",
            "model": "kimi-k3-1-pro",
            "input": "Write a full specification for a task scheduler.",
            "max_output_tokens": 100000,
        }))
        async for message in ws:
            event = json.loads(message)
            if event["type"] == "response.output_text.delta":
                print(event["delta"], end="", flush=True)
            elif event["type"] in DONE:
                break


asyncio.run(main())
```

A request that cannot run is answered with an `error` event, and the connection stays open for your next request:

```json
{
  "type": "error",
  "status": 404,
  "code": "model_not_found",
  "message": "Model 'no-such-model' is not available. Call GET /v1/models for the current catalog.",
  "param": "model",
  "sequence_number": 0,
  "error": {
    "type": "invalid_request_error",
    "code": "model_not_found",
    "message": "Model 'no-such-model' is not available. Call GET /v1/models for the current catalog.",
    "param": "model"
  }
}
```

## Limits and behavior

| Item               | Limit                                                                                                                                                            |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| One response       | Runs for up to 60 minutes.                                                                                                                                       |
| One connection     | Accepts new requests for 60 minutes. A response that is running finishes, then open a new connection.                                                            |
| At once            | 16 responses on a connection.                                                                                                                                    |
| Named lanes        | 32 `stream_id` values on a connection. Requests on one lane run in order, different lanes run together, and every event of a named lane carries its `stream_id`. |
| Prompts and output | The same limits as `POST /v1/responses` for that model. Use a high `max_output_tokens` for a long response.                                                      |

Requests are the same as `POST /v1/responses`, with these differences:

* Send the full conversation in `input` with every request. Responses are not stored, so `previous_response_id` returns `previous_response_not_found`, and `generate: false` returns `400`.
* `background: true` is not available, here or over HTTP.
* If the connection drops, the rest of the response does not reach you, and the request bills under the [cancellation policy](/billing-credits#cancellation-policy). Send the request again to get it.

## Billing

Each response is billed as its own request, at the same rates as `POST /v1/responses`, and appears on its own line in your usage. Opening a connection costs nothing.

## At the 15 minute HTTP limit

A request over HTTP that reaches 15 minutes ends with a `timeout` error that names this page's endpoint, on every text endpoint:

* A stream sends what was written so far, then the error event of its protocol.
* A request without `stream` returns the error in its JSON body.

The request bills under the [cancellation policy](/billing-credits#cancellation-policy). Send it over the WebSocket to run it to the end.