--- title: Configure WebSocket methods in your SDKs | Stainless description: Generate SDK methods for WebSocket endpoints that support full-duplex, bi-directional communication with typed client and server events. --- WebSocket method generation is in early access and currently supports **TypeScript** and **Python**. The interface may change as the feature develops. [Contact us](mailto:feedback@stainless.com) if you have any questions not addressed by the documentation below. [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) provide full-duplex, bi-directional communication between client and server over a persistent connection, enabling real-time data exchange after an initial HTTP handshake. Unlike [SSE streaming](/docs/sdks/configure/streaming/index.md), WebSockets allow both the client and server to send messages at any time. OpenAPI can define the initial HTTP handshake endpoint and the message schemas, but cannot express which schemas are used for client and server messages in a WebSocket connection. Your Stainless config bridges that gap by specifying which schemas correspond to each direction of communication, enabling Stainless to generate fully typed code. ## Configure a WebSocket method To define a WebSocket method, add a method with `type: websocket` to your Stainless config: ``` resources: feed: methods: connect: type: websocket path: /feed client_event_schema_uri: "#/components/schemas/ClientEvent" server_event_schema_uri: "#/components/schemas/ServerEvent" ``` The `client_event_schema_uri` and `server_event_schema_uri` reference schemas defined in your OpenAPI spec. The `server_event_schema_uri` must be a discriminated union so the SDK can distinguish between different incoming event types at runtime. The `client_event_schema_uri` can be any schema, but using a discriminated union is recommended so that clients get typed event variants. Here is an example of a client event schema defined as a discriminated union in OpenAPI: ``` components: schemas: ClientEvent: oneOf: - $ref: "#/components/schemas/SubscribeEvent" - $ref: "#/components/schemas/SendMessageEvent" discriminator: propertyName: type mapping: subscribe: "#/components/schemas/SubscribeEvent" send_message: "#/components/schemas/SendMessageEvent" SubscribeEvent: type: object required: [type, channel] properties: type: type: string enum: [subscribe] channel: type: string SendMessageEvent: type: object required: [type, channel, content] properties: type: type: string enum: [send_message] channel: type: string content: type: string ``` The `discriminator` field with `propertyName` tells Stainless (and OpenAPI tooling) which property distinguishes the union members. Each member schema must include that property with a unique constant value. ### Method properties | Property | Required | Description | | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Yes | Must be `websocket`. | | `path` | Yes | The path to the WebSocket handshake endpoint. Supports path parameters, e.g. `/feed/{channel_id}`. | | `client_event_schema_uri` | Yes | A `$ref`-style URI pointing to the schema for messages the client can send. Must be a discriminated union. | | `server_event_schema_uri` | Yes | A `$ref`-style URI pointing to the schema for messages the server can send. Must be a discriminated union. | | `query_params_schema_uri` | No | A `$ref`-style URI pointing to the schema for query parameters used when initializing the connection. | | `event_name_separator` | No | **Python only.** Separator character in event names (e.g. `.` for `room.send_message`). When set, generates typed nested helper methods in addition to the universal `send` method. See [Event helper methods](#event-helper-methods). | Only one WebSocket method is allowed per resource. If you need multiple WebSocket endpoints, place them on separate subresources. ### Connection parameters If your WebSocket endpoint accepts query parameters during the handshake, specify them with `query_params_schema_uri`: openapi.yml ``` components: schemas: FeedConnectParams: type: object required: [token] properties: token: type: string description: Session token used to authenticate the connection. channel: type: string description: Channel to subscribe to on connect. ``` stainless.yml ``` resources: feed: methods: connect: type: websocket path: /feed client_event_schema_uri: "#/components/schemas/ClientEvent" server_event_schema_uri: "#/components/schemas/ServerEvent" query_params_schema_uri: "#/components/schemas/FeedConnectParams" ``` The properties of the referenced schema become typed parameters on the generated method. - [TypeScript](#tab-panel-22) - [Python](#tab-panel-23) ``` import { FeedWS } from 'acme/resources/feed/ws'; const ws = new FeedWS(client, { // Required token: 'user-session-token', // Optional channel: 'general', }); ``` ``` from acme import Acme client = Acme() client.feed.connect( # Required token='user-session-token', # Optional channel='general', ) ``` ### Event helper methods Every connection exposes a `send` method that accepts any message matching your `client_event_schema_uri` schema: ``` connection.send({"type": "room.send_message", "channel": "general", "content": "hello"}) ``` In Python, if your client event names share a consistent separator, set `event_name_separator` to generate typed helper methods that mirror your event hierarchy. For example, with events named like `room.send_message`: ``` resources: chat: methods: connect: type: websocket path: /chat client_event_schema_uri: "#/components/schemas/ClientEvent" server_event_schema_uri: "#/components/schemas/ServerEvent" event_name_separator: "." ``` The SDK generates a `room` namespace on the connection object: ``` # Equivalent to connection.send({"type": "room.send_message", ...}) connection.room.send_message(channel="general", content="hello") ``` The `send` method remains available regardless of whether `event_name_separator` is set. ## Reconnection The generated SDKs support reconnection with exponential backoff for recoverable connection failures. To enable reconnection, you must provide an `onReconnecting` (TypeScript) or `on_reconnecting` (Python) handler. This handler is required because the SDK cannot guarantee that reconnecting is safe in the general case — your server may require a new session ID, updated authentication tokens, or other state that must be refreshed before re-establishing the connection. - [TypeScript](#tab-panel-24) - [Python](#tab-panel-25) Pass a `reconnect` option with an `onReconnecting` handler when creating the WebSocket: ``` import { FeedWS } from 'acme/resources/feed/ws'; const ws = new FeedWS(client, { token: 'user-session-token' }, { reconnect: { maxRetries: 5, initialDelay: 500, maxDelay: 8000, onReconnecting(event) { console.log(`Reconnecting (attempt ${event.attempt}/${event.maxAttempts})...`); // Optionally return { parameters: { ... } } to override query parameters }, }, }); ``` Provide an `on_reconnecting` callback to the `connect()` method: ``` from acme import Acme client = Acme() def handle_reconnecting(event): print(f"Reconnecting (attempt {event.attempt}/{event.max_attempts})...") # Optionally return {"abort": True} to stop reconnecting # Or return {"extra_query": {...}, "extra_headers": {...}} to override with client.feed.connect( token="user-session-token", on_reconnecting=handle_reconnecting, max_retries=5, initial_delay=0.5, max_delay=8.0, ) as connection: # ... ``` If reconnection is always safe for your API (for example, because the server handles session resumption transparently), you can provide a minimal handler: - [TypeScript](#tab-panel-26) - [Python](#tab-panel-27) ``` const ws = new FeedWS(client, { token: 'user-session-token' }, { reconnect: { onReconnecting() {}, }, }); ``` ``` with client.feed.connect( token="user-session-token", on_reconnecting=lambda event: None, ) as connection: # ... ``` If reconnection is always safe for every consumer of your SDK, you can use [custom code](/docs/sdks/configure/custom-code/index.md) to add a default reconnection handler so that end users do not need to provide one themselves. The SDK automatically reconnects on these close codes: - `1001` (Going Away) - `1005` (No Status) - `1006` (Abnormal Closure) - `1011` (Internal Error) - `1012` (Service Restart) - `1013` (Try Again Later) - `1015` (TLS Handshake Failure) Messages sent during reconnection are held in an in-memory send queue (default 1 MB) and flushed once the new connection is established. ## How to use WebSockets in your SDKs The example code below shows how your users can use WebSockets after you configure them and provide new builds of your SDKs. - [TypeScript](#tab-panel-28) - [Python](#tab-panel-29) The TypeScript SDK generates separate entry points for Node.js and browser environments. **Node.js** (requires the `ws` package): **Event-based API:** ``` import { FeedWS } from 'acme/resources/feed/ws'; const ws = new FeedWS(client, { token: 'user-session-token' }); // Listen for specific server event types ws.on('message_received', (event) => { console.log(`${event.channel}: ${event.content}`); }); ws.on('error', (err) => { console.error('WebSocket error:', err); }); // Send typed client events ws.send({ type: 'subscribe', channel: 'general' }); ws.send({ type: 'send_message', channel: 'general', content: 'hello' }); // Close when done ws.close(); ``` **Async iterator** (alternative to the event-based API): ``` for await (const event of ws) { switch (event.type) { case 'message': console.log('Received:', event.message); break; case 'close': console.log('Connection closed'); break; } } ``` The iterator yields lifecycle and message events until the connection closes. To shut down cleanly, call `ws.close()` from an event handler or external signal — the iterator will then complete. **Browser** (uses the native WebSocket API): ``` import { FeedWS } from 'acme/resources/feed/ws-browser'; const ws = new FeedWS(client, { token: 'user-session-token' }); ``` A browser client is only generated when authentication is compatible with browser WebSocket limitations. The browser `WebSocket` API does not support custom HTTP headers, so Bearer tokens and other header-based auth schemes are not available. Browser clients are generated for APIs that are unauthenticated, use query-parameter auth, or authenticate via the `Sec-WebSocket-Protocol` header. When a default `Sec-WebSocket-Protocol` header is configured, the generated browser class seeds the protocol value automatically. The Python SDK generates both sync and async connection classes. The `connect()` method returns a context manager: **Pull-based iteration** — process events in a loop: ``` async with client.feed.connect(token="user-session-token") as connection: await connection.send({"type": "subscribe", "channel": "general"}) async for event in connection: print(event.type, event) ``` The `async for` loop blocks until the connection closes, so send any initial messages before entering it. **Push-based event handlers** — register callbacks and dispatch: ``` async with client.feed.connect(token="user-session-token") as connection: @connection.on("message_received") async def on_message(event): print(f"{event.channel}: {event.content}") await connection.send({"type": "subscribe", "channel": "general"}) await connection.dispatch_events() ``` `dispatch_events()` blocks until the connection closes, dispatching each event to registered handlers. Use one approach or the other — not both. **Event helper methods** (when `event_name_separator` is set): ``` async with client.feed.connect(token="user-session-token") as connection: # Instead of: await connection.send({"type": "room.send_message", ...}) await connection.room.send_message(channel="general", content="hello") ``` **Sync usage:** ``` with client.feed.connect(token="user-session-token") as connection: connection.send({"type": "subscribe", "channel": "general"}) for event in connection: print(event.type, event) ``` The Python SDK requires the `websockets` library. Install it with `pip install acme[websockets]`. ## Language-specific configuration ### Python You can customize the pip extras group name for the WebSocket dependency: ``` targets: python: options: websockets_extra_name: realtime ``` This changes the install command from `pip install acme[websockets]` to `pip install acme[realtime]`. ## Tips - Both client and server event schemas must be discriminated unions — the SDK uses the discriminator to route and type-check events - Use `event_name_separator` when event names have hierarchical structure (e.g. `room.send_message`) to generate ergonomic nested helper methods - Only one WebSocket method is allowed per resource — use subresources if you need multiple WebSocket endpoints - Define connection parameters in a separate schema when your handshake endpoint requires authentication tokens or session identifiers - Browser WebSocket clients (TypeScript only) support unauthenticated, query-parameter, and `Sec-WebSocket-Protocol` auth — Bearer tokens and other custom headers are not available in browser WebSocket connections