---
title: "Real-time pose streaming (WebSocket — on-demand)"
method: GET
path: "/v1/pose/stream"
tags: ["Pose"]
---

# Real-time pose streaming (WebSocket — on-demand)

`GET /v1/pose/stream`

## Request

- Base URL: `http://localhost:9998`
- URL: `http://localhost:9998/v1/pose/stream`
- Auth: HTTP bearer

Dedicated WebSocket endpoint for 60fps skeleton streaming.

**The WebSocket server is NOT started at boot.** Call `POST /v1/pose/stream/start`
first — the response includes the actual port and endpoint URL. The server
prefers port 10005 but uses an OS-assigned port if 10005 is occupied.

**Endpoint:** `ws://127.0.0.1:<port>` (separate port, not on 9998)
**Auth:** Bearer token via the `Sec-WebSocket-Protocol` upgrade header.
Open with `new WebSocket(url, ['bearer.toolpiper.v1', 'bearer.<token>', 'corr.<uuid>'])`
— see `docs/archive/local-http-auth-bearer-only-paths.md` § Phase 2.
**Max concurrent:** 4 streams

## Lifecycle

1. `POST /v1/pose/stream/start` — starts the WebSocket server, returns `{ port, endpoint }`
2. Connect to the returned `endpoint` URL
3. `GET /v1/pose/stream/status` — check running state and connection count
4. `POST /v1/pose/stream/stop` — stops the server (or let it run)

## Client → Server

| Message | WS Type | Description |
|---------|---------|-------------|
| `{"type":"config", ...}` | text | Configure session (optional — defaults apply) |
| Raw JPEG/PNG bytes | binary | One frame per message |
| `{"type":"record","action":"start"}` | text | Start recording to ring buffer |
| `{"type":"record","action":"stop"}` | text | Stop recording |
| `{"type":"eos"}` | text | End session |

### Config message (all fields optional)
```json
{
  "type": "config",
  "backend": "apple-vision",
  "topology": "openpose-18",
  "format": "compact-binary",
  "include_hands": false,
  "include_face": false,
  "detect_3d": false,
  "confidence_threshold": 0.3,
  "canvas_width": 1024,
  "canvas_height": 1024,
  "renderer": "openpose",
  "stats_interval_ms": 1000
}
```

## Server → Client

| Message | WS Type | Description |
|---------|---------|-------------|
| `{"type":"config_ack", "joint_order":[...]}` | text | Resolved config + joint order for binary decoding |
| Pose data | binary or text | Format depends on config (see Output Formats) |
| `{"type":"stats", ...}` | text | Periodic fps, drop rate, avg detection time |
| `{"type":"record_ack", ...}` | text | Recording start/stop confirmation |
| `{"type":"error","message":"..."}` | text | Error |

## Output Formats

**`compact-binary`** (default) — Binary WS message. Pre-allocated buffer, zero heap alloc per frame.
236 bytes for 1 person / OpenPose-18 / 2D:
- Header (16B): magic `0x504F5345`, version, flags, num_people, num_joints, detection_ms, frame_seq
- Per-person: optional bbox (4×f32), confidence (f32), per-joint x/y/confidence (3×f32 each, +z if 3D)

**`compact-json`** — Text WS message. Short keys, flat arrays, pixel-space coords.
`{"f":42,"ms":8.3,"n":1,"p":[[512.0,307.2,0.95,...]]}`

**`verbose-json`** — Text WS message. Named joints, full metadata.
`{"type":"pose","frame_seq":42,"detection_ms":8.3,"people":[{"keypoints":{"nose":{"x":0.5,"y":0.3,"c":0.95},...}}]}`

**`image`** — Binary WS message (PNG). Rendered skeleton via selected renderer. Cannot sustain 60fps.

## Other responses

- `101`: WebSocket upgrade (connect to the port returned by /v1/pose/stream/start)

---

[API](https://skmtc.dev/modelpiper/apis/toolpiper-api.md) · [All operations](https://skmtc.dev/modelpiper/apis/toolpiper-api/llms.txt) · [OpenAPI document](https://skmtc.dev/modelpiper/apis/toolpiper-api/revisions/6722bf56385c?raw)
