WebSocket /stream/subscribe
Wire-level reference for client-side insight delivery.
The endpoint client apps use to receive live insights.
wss://api.raeh.io/stream/subscribeQuery parameters
| Name | Required | Description |
|---|---|---|
api_key | ✓ | Your raeh_* API key. Determines which account's insights you receive. |
No device_id, no session_id, no filter parameters. The subscription is account-scoped; filter client-side if you only care about a specific session or device.
Subscribe ack
Right after the WebSocket opens, the server sends one small JSON message:
{ "status": "subscribed" }Drain it and discard. Every subsequent message is an insight.
Insight payload
{
"type": "hr",
"value": 72.4,
"unit": "bpm",
"ts": 1760000000000,
"session_id": "ad225407-36de-48b1-bcf2-14d5c057aeca",
"device_id": "xyz-watch-a3b5",
"confidence": 0.87,
"sqi_class": "excellent"
}| Field | Type | Description |
|---|---|---|
type | string | hr, spo2, or rr. Treat unknown types as future-compatible noise. |
value | number | Reading in the unit below. |
unit | string | bpm for HR, % for SpO₂, brpm for RR. |
ts | number | Publish time (ms since epoch). |
session_id | string | UUID of the session the insight came from. |
device_id | string | External device ID; the same string passed to /stream/ingest. |
confidence | number | Signal Quality Index, 0..1. How much of the analysis window was actually covered by fresh, gap-free data. Higher is better. |
sqi_class | string | Discrete bucket: "excellent" (≥ 0.8), "acceptable" (≥ 0.5), or "unfit" (< 0.5). Matches the PPG SQI literature. |
Cadence and starting delay: see Building a Client App → What insights you can expect.
How to read confidence / sqi_class
Every algorithm in the pipeline emits a number, but not every number is equally trustworthy. confidence is a continuous quality score summarizing how much of the requested analysis window was actually filled by in-window, low-gap data from the input the algorithm depends on. It's derived as coverage × (1 − gap_fraction) and clamped to [0, 1].
Recommended UI treatment:
sqi_class == "excellent": show the value at full opacity, no annotation.sqi_class == "acceptable": show the value, optionally dim it slightly (e.g. 70% opacity) to indicate a hint of uncertainty.sqi_class == "unfit": don't show the value as if it's authoritative; either hide it, show a dash, or label it "estimating…".
confidence can also come from algorithm-specific logic (e.g. R-peak regularity for HR), in which case it overrides the generic geometric score. Treat the buckets as the canonical UI contract; the continuous number is for finer-grained animations or thresholds.
Close codes
| Code | Reason | When |
|---|---|---|
| 1000 | Normal closure | Either side cleanly closed. |
| 4001 | Authentication failed | api_key missing, invalid, revoked, or bound to a different account. |
Subscription longevity
The subscription is intended to be long-lived. There's no server-enforced idle timeout today. Typical pattern:
- Client opens on app foreground.
- Streams insights as long as the connection is open.
- Reconnects with exponential backoff on network drop.
- Client closes on app background or user logout.
Insights published while the subscription is disconnected are not replayed on reconnect. If you need to reconcile the gap, query historical insights via REST:
GET /v1/sessions/{session_id}/insightsNo filtering parameters (yet)
A single subscription receives insights from every session under the account. A watch under user A's phone and a watch under user B's phone both deliver to a key scoped to the shared account. In practice:
- Per-user keys (recommended for production): each user's key only sees their sessions naturally.
- Shared key + client-side filter: filter on
session_idordevice_idin the app.
Server-side subscription filters (e.g. ?session_id=...) are on the roadmap.
Multiple subscribers
Several clients can subscribe concurrently with the same key. Every insight is delivered to every subscriber. Example: a phone app + a web dashboard both listening; both see every reading.
Heartbeats
No server-side ping is sent today. If you're running in an environment where idle TCP connections get dropped by an intermediary (corporate proxy, mobile carrier NAT), send a WebSocket ping from the client side every 30–60 s and watch for the pong.