Skip to main content

Server-Sent Events (SSE) Streaming

Some APIs stream their response incrementally over a long-lived connection using Server-Sent Events (SSE) instead of returning a single buffered body. Typical examples include chat-completion and other AI APIs that emit tokens as they're generated. APIMatic detects these operations from the API contract and generates an endpoint that returns a typed, async-iterable stream, so consumers can process events as they arrive using their language's natural async iteration loop.

note

SSE streaming is currently supported in TypeScript and .NET (v4 beta) SDKs. The generated interface follows the same model in both languages but adapts to each language's idioms, as shown in the examples below.

SSE Configuration in OpenAPI

No special extension is required. An operation is treated as a streaming endpoint whenever its success response declares a text/event-stream media type. The schema associated with that media type describes the shape of each event's payload, and the generated SDK decodes every incoming frame into that type.

paths:
/chat/completions:
post:
operationId: createChatCompletion
responses:
'200':
description: OK
content:
text/event-stream:
schema:
$ref: '#/components/schemas/CreateChatCompletionStreamResponse'

Example Usage

For a streaming endpoint, the generated method returns a stream of decoded events of type T, where T is the schema associated with the text/event-stream response. The stream is single-use and lazy: it's consumed with the language's async iteration construct, and each iteration yields the next decoded event as it arrives over the wire.

The endpoint method returns a Promise<ApiResponse<SseStream<T>>>. The result property is the SseStream<T>, a single-use AsyncIterable<T>.

const body: CreateChatCompletionRequest = {
messages: [{ role: 'user', content: 'Hello!' }],
model: 'gpt-5.4',
stream: true,
};

try {
const response = await chatController.createChatCompletion(body);

// Asynchronously iterating over the server-sent events as they arrive.
for await (const event of response.result) {
console.log(event);
}

// Extracting response status code.
console.log(response.statusCode);
// Extracting response headers.
console.log(response.headers);
} catch (error) {
console.log(error);
}

In this example:

  • response: The ApiResponse returned by the endpoint. Its result property is the SseStream<T>.
  • response.result: The SseStream<T>. It behaves as an AsyncIterable<T>, allowing direct iteration over decoded events using for await...of.
  • event: A single decoded event payload of type T.
  • response.statusCode: The HTTP status code returned when the stream was opened.
  • response.headers: The HTTP headers returned when the stream was opened.
  • Error Handling: Wrap the streaming logic in try-catch blocks to catch and handle ApiError, as well as the SSE-specific errors described below.

Accessing Event Metadata

note

Access to raw SSE frame metadata is currently a TypeScript capability. In .NET, the stream yields decoded events directly, and streams that mix multiple event shapes are modeled as a oneOf union type.

SSE frames can carry metadata in addition to the payload: an id, an event name, and a retry reconnection hint. Iterating the stream directly yields only the decoded payloads; accessing this metadata is shown below.

Iterate the stream via withMetadata(), which yields full SseEvent<T> objects.

const response = await chatController.createChatCompletion(body);

// Iterate full events, including `id`, `event` and `retry` metadata.
for await (const event of response.result.withMetadata()) {
console.log(event.data); // The decoded payload of type `T`.
console.log(event.id); // The `id:` field value in effect for this event, if any.
console.log(event.event); // The `event:` field value, if any.
console.log(event.retry); // The `retry:` reconnection delay in milliseconds, if any.
}

In this example:

  • event.data: The decoded event payload of type T.
  • event.event: The event: field value, if any.
  • event.id: The id: field value in effect for this event, if any.
  • event.retry: The retry: reconnection delay, in milliseconds, if any.

Closing the Stream

The stream is backed by a live HTTP connection. It's single-use and lazy, so the connection is held only while it's being consumed. Stopping consumption releases the connection.

Breaking out of the iteration loop, or calling close() explicitly, aborts the underlying request.

const response = await chatController.createChatCompletion(body);

for await (const event of response.result) {
console.log(event);
if (shouldStop(event)) {
response.result.close(); // Aborts the connection and stops iteration.
break;
}
}

Read Timeout Configuration

To guard against a server that opens a stream but then stalls indefinitely, streaming SDKs expose a read-timeout client configuration option. It bounds the maximum idle window allowed between two consecutive frames, and never the caller's own processing time. If no frame arrives within that window, the stream fails with a timeout error. The default is 60 seconds.

The streamReadTimeout option is a number of milliseconds (default 60000).

const client = new Client({
// Fail if the server stalls for more than 30 seconds between frames.
streamReadTimeout: 30000,
});

Error Handling

In addition to the usual API error, streaming endpoints can surface two SSE-specific errors while the stream is being consumed:

  • A timeout error, thrown when the server stalls between frames for longer than the configured read timeout. It exposes the idle window that elapsed without a frame arriving.
  • A decode error, thrown when a frame's payload can't be decoded into the expected type T. It exposes the raw frame payload that failed, along with the underlying cause.

The errors are SseTimeoutError (with idleTimeoutMs) and SseDecodeError (with rawFrame and cause). Both extend the SseError base type.

import { SseDecodeError, SseTimeoutError } from 'your-sdk';

try {
const response = await chatController.createChatCompletion(body);
for await (const event of response.result) {
console.log(event);
}
} catch (error) {
if (error instanceof SseTimeoutError) {
// The server stalled longer than `streamReadTimeout`.
console.log(error.idleTimeoutMs);
} else if (error instanceof SseDecodeError) {
// A frame could not be decoded into the expected type.
console.log(error.rawFrame, error.cause);
} else {
console.log(error);
}
}

Benefits

  • Typed Events: Every frame is decoded into the response schema type, so consumers work with typed objects rather than raw text.
  • Familiar Interface: The stream is a standard async sequence, consumed with the language's natural async iteration loop.
  • OpenAPI-Driven: Automatically applied to any operation with a text/event-stream response, with no extra configuration required.
  • Safe by Default: A configurable read timeout prevents stalled connections from hanging indefinitely, and the connection is released as soon as iteration stops.