Configure WebSocket methods in your SDKs
Generate SDK methods for WebSocket endpoints that support full-duplex, bi-directional communication with typed client and server events.
WebSockets 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, 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
Section titled “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: stringThe 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
Section titled “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. |
Connection parameters
Section titled “Connection parameters”If your WebSocket endpoint accepts query parameters during the handshake, specify them with query_params_schema_uri:
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.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.
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
Section titled “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
Section titled “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.
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:
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 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
Section titled “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.
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 typesws.on('message_received', (event) => { console.log(`${event.channel}: ${event.content}`);});
ws.on('error', (err) => { console.error('WebSocket error:', err);});
// Send typed client eventsws.send({ type: 'subscribe', channel: 'general' });ws.send({ type: 'send_message', channel: 'general', content: 'hello' });
// Close when donews.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' });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)Language-specific configuration
Section titled “Language-specific configuration”Python
Section titled “Python”You can customize the pip extras group name for the WebSocket dependency:
targets: python: options: websockets_extra_name: realtimeThis changes the install command from pip install acme[websockets] to pip install acme[realtime].
- Both client and server event schemas must be discriminated unions — the SDK uses the discriminator to route and type-check events
- Use
event_name_separatorwhen 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-Protocolauth — Bearer tokens and other custom headers are not available in browser WebSocket connections