> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inputapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Responses WebSocket integration

> Complete two turns on one connection, validate results, and replay history.

This path upgrades `GET /v1/responses` to WebSocket. Confirm model, upstream, and channel support; the administrator must enable channel `ResponsesWebSocketEnabled`. Working HTTP Responses calls do not prove bidirectional support.

## Runnable two-turn text example

Install dependencies and configure the environment using [connection and runtime](/en/guides/websocket), then run:

```sh theme={null}
node examples/responses-websocket.mjs
```

The script asks for a coffee-shop name and then an explanation. It sends turn two only after validating turn one's terminal event. Output is published per completed turn, so turn one's text remains if turn two fails. Keep `websocket-client.mjs` alongside the copied script.

```javascript theme={null}
import { openWebSocket } from './websocket-client.mjs';

let session;
try {
  session = await openWebSocket('responses');
  const history = [];
  const seenResponses = new Set();
  for (const prompt of ['Suggest a short name for a coffee shop.', 'Explain that name in one sentence.']) {
    history.push({ role: 'user', content: prompt });
    await session.send({
      type: 'response.create', model: session.model,
      input: history, instructions: 'Answer briefly in plain text.',
      store: false, include: ['reasoning.encrypted_content'],
    });
    let responseId;
    const textParts = new Map();
    const itemIds = new Map();
    let textBytes = 0;
    for (;;) {
      const event = await session.receive();
      if (event.type === 'response.created') {
        const id = event.response?.id;
        if (responseId || typeof id !== 'string' || !id || seenResponses.has(id)) throw new Error('Invalid response identity.');
        responseId = id;
        seenResponses.add(id);
      } else if (event.type === 'response.output_text.delta') {
        if (!responseId || (event.response_id && event.response_id !== responseId) || typeof event.delta !== 'string'
            || typeof event.item_id !== 'string' || !event.item_id
            || !Number.isInteger(event.output_index) || event.output_index < 0
            || !Number.isInteger(event.content_index) || event.content_index < 0) throw new Error('Invalid text delta.');
        const key = `${event.output_index}:${event.content_index}`;
        if (itemIds.has(event.output_index) && itemIds.get(event.output_index) !== event.item_id) throw new Error('Mixed output items.');
        itemIds.set(event.output_index, event.item_id);
        textParts.set(key, (textParts.get(key) ?? '') + event.delta);
        textBytes += Buffer.byteLength(event.delta);
        if (textBytes > 1024 * 1024) throw new Error('Text limit exceeded.');
      } else if (['response.completed', 'response.done'].includes(event.type)) {
        const result = event.response;
        if (!responseId || result?.id !== responseId || result.status !== 'completed' || result.error
            || !Array.isArray(result.output) || !result.output.length) throw new Error('Response did not complete.');
        const finalParts = new Map();
        for (const [index, item] of result.output.entries()) {
          if (item.type === 'reasoning') continue;
          if (item.type !== 'message' || item.id !== itemIds.get(index) || item.role !== 'assistant' || item.status !== 'completed'
              || !Array.isArray(item.content) || !item.content.length) throw new Error('Unsupported output or tool call.');
          for (const [partIndex, part] of item.content.entries()) {
            if (part.type !== 'output_text' || typeof part.text !== 'string') throw new Error('Refusal or invalid text.');
            finalParts.set(`${index}:${partIndex}`, part.text);
          }
        }
        if (finalParts.size !== textParts.size
            || [...finalParts].some(([key, text]) => textParts.get(key) !== text)) throw new Error('Final text differs from deltas.');
        const text = [...finalParts.values()].join('\n');
        if (!text.trim()) throw new Error('Empty response.');
        // Replay every raw output item, preserving encrypted reasoning and metadata.
        history.push(...result.output);
        console.log(text);
        break;
      } else if (['response.failed', 'response.incomplete', 'response.cancelled', 'response.canceled'].includes(event.type)) {
        throw new Error('Response ended without success.');
      } else if (event.response_id && event.response_id !== responseId) {
        throw new Error('Unexpected response identity.');
      }
    }
  }
} catch {
  console.error('Responses WebSocket failed. Check configuration, handshake, events, and deadline; no automatic retry.');
  process.exitCode = 1;
} finally {
  session?.close();
}
```

## History, deltas, and completion

The example replays user messages and all raw output items, preserving encrypted reasoning, message phase, and extra metadata. It resends instructions each turn. The model must support `reasoning.encrypted_content`; adapt to its protocol if needed rather than dropping returned reasoning state.

The receiver obtains the ID from `response.created` and correlates text by output\_index / content\_index / item\_id. Responses text deltas do not guarantee response\_id; the example checks it when present. The terminal object must match the current response and its text must equal collected deltas. Empty output, refusals, tool calls, and unfinished messages are rejected as text answers.

For `previous_response_id`, send new input and verify that upstream connection state is still available. Do not assume caches survive reconnects. Stateless recovery can replay full history or a [compacted window](/en/guides/compaction), but the application must decide whether to resubmit. See the official [WebSocket mode](https://developers.openai.com/api/docs/guides/websocket-mode) background.

## Gateway and upstream capabilities differ

This project permits only one active generation per connection and locks the established model and channel. Do not import newer official promises of multiple stream\_id lanes, queuing, or forks. Open a new connection to change models. Verify incremental continuation with the actual channel. Handshake and each create can fail as access, rate limits, or channel configuration changes.

Warmup with `generate: false` and provider extensions are outside the example. `stream` and `background` are not WebSocket transport switches. Client tools need separate argument assembly, execution, result association, and recovery handling; this text example rejects tool output.

## Cancellation and recovery

The API accepts `response.cancel` controls for an active generation, but success depends on the upstream response. Correlate control errors separately from generation errors; sending cancel is not confirmation. The example sends no automatic cancellation. Closing after timeout does not guarantee a refund or upstream interruption.

[Handshake, fields, and events](/en/api-reference/responses-websocket) · [Troubleshooting](/en/guides/troubleshooting)

[阅读中文](/zh/guides/responses-websocket)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.