Real-Time Streaming
Section titled “Real-Time Streaming”Subscribe to in-container notifications in real time. The primary transport is a WebSocket: the client sends an upgrade request and receives 101 Switching Protocols, then reads JSON notification and heartbeat messages from the open socket. A non-upgrade GET request receives a 200 text/event-stream response as a fallback. Use this when the client needs push-based updates without polling. For background on the notification model and display IDs, see the Notification Server overview.
Connect to the notification stream
Section titled “Connect to the notification stream”GET /api/v1/notifications/stream
Section titled “GET /api/v1/notifications/stream”Establishes a WebSocket connection for real-time notification updates. The server replies with a 101 Switching Protocols response, after which the client receives a continuous stream of notification messages and periodic heartbeat messages for the subscribed displays. Pass a comma-separated list of display IDs in the displays query parameter, or all to subscribe to every display in the container. Display IDs start at 1; there is no display :0.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
displays | query | string | No | Comma-separated display IDs (1,:2,3), or all / * for every display. Required for SSE (400 without it). Optional for WebSocket: without it the socket receives nothing until the client sends a subscribe message; an invalid ID arrives as an error frame after the upgrade. |
Response
Section titled “Response”A successful handshake returns 101 Switching Protocols. The response carries no body; after this point the connection is a WebSocket and the client begins receiving notification and heartbeat messages.
A non-upgrade GET request receives a 200 OK response with a text/event-stream body. Each event’s data field is one JSON message. The first message is connected, followed by periodic heartbeat messages and any new notifications. The retry field tells EventSource to reconnect after 3 seconds. The stream ends when the server shuts down.
retry: 3000data: {"type":"connected","displays":["1"]}
data: {"type":"heartbeat","timestamp":1749025022950}The request is malformed, typically because the displays query parameter is missing (SSE only) or contains invalid values.
{ "details": "Invalid display ID format", "error": "Validation Error", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | Request parameters failed validation | Check parameter format and try again |
INVALID_DISPLAY_ID | Invalid display ID | Display ID format is incorrect | Use format: “1”, “:1”, or comma-separated list like “1,:2,3” |
The WebSocket upgrade request came from an Origin not listed in WS_ALLOWED_ORIGINS. This check is performed before the upgrade completes; SSE requests are not origin-checked.
{ "success": false, "error": "Forbidden", "message": "Origin not allowed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ORIGIN_NOT_ALLOWED | WebSocket origin not allowed | The upgrade request’s Origin header is not in the server’s WS_ALLOWED_ORIGINS list | Connect from an allowed origin, or ask the operator to add it to WS_ALLOWED_ORIGINS |
The client IP has exceeded the rate limit for this endpoint. Limits are fixed windows per client IP (RATE_LIMIT_WINDOW_MS, default 60 s): RATE_LIMIT_MAX_REQUESTS (60) for every route, plus RATE_LIMIT_NOTIFY_MAX (10) for POST /notify and RATE_LIMIT_ICONS_MAX (100) for icons. CORS preflights (OPTIONS) are never counted. A Retry-After header indicates the whole seconds until the current window ends.
{ "success": false, "error": "Rate Limit Exceeded", "message": "You have exceeded the 60 requests in 1 minutes limit on this endpoint."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | Rate limit exceeded | Too many requests from your IP address | Wait the number of seconds in the Retry-After header, then retry. Default limits per client IP per minute: 60 for every route, 10 for notify, 100 for icons |
The server is shutting down and accepts no new streams.
{ "success": false, "error": "Service Unavailable", "message": "Server is shutting down"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVER_SHUTTING_DOWN | Server shutting down | The server is stopping (for example during a deploy) and refuses new stream connections | Reconnect with backoff; the restarted server accepts the connection |
Subscribe to specific displays
Section titled “Subscribe to specific displays”The endpoint upgrades HTTP to WebSocket. curl will perform the handshake, but maintaining the open socket and consuming messages requires a WebSocket-capable client. The example below shows the upgrade request with the displays query parameter.
curl -i \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ "https://{projectId}-{containerId}-n-1.{server}.containers.hoody.com/api/v1/notifications/stream?displays=1,2,3"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-n-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notifications.connect({ displays: '1,2,3' });Subscribe to every display
Section titled “Subscribe to every display”curl -i \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ "https://{projectId}-{containerId}-n-1.{server}.containers.hoody.com/api/v1/notifications/stream?displays=all"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-n-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notifications.connect({ displays: 'all' });