Skip to content

WebSocket

Two WebSocket endpoints for different use cases.

Comparison

REST /ws (:22222) Core WS (--websocket-port :44444)
Protocol JSON frames (base64(body)) FTAPI binary (44-byte header + protobuf body)
Purpose Push-only (server → client) Full-duplex (Futu SDK compatible)
Client Browser / websocat / scripts Official futu-api SDK (over WS transport)
Auth ?token= or Authorization: Bearer on handshake same
Per-message scope Yes (v1.0+, checked by proto_id)

REST /ws — push only

For browser frontends and lightweight scripts.

// Browser (WebSocket API can't set headers, use ?token=)
const ws = new WebSocket('ws://localhost:22222/ws?token=fc_xxxx...')

ws.onmessage = (ev) => {
  const event = JSON.parse(ev.data)
  // event: { type: 'quote' | 'trade' | 'notify',
  //          proto_id, sec_key?, sub_type?, acc_id?, body_b64 }
  const body = atob(event.body_b64)  // protobuf bytes
  // ... decode body
}

v0.9+: the server filters pushes by the client's scope:

  • qot:read-only key → receives quote + notify only, no trade
  • qot:read + acc:read → receives all three

Core WS — full-duplex binary

# enable the port at startup
./futu-opend --websocket-port 44444 --ws-keys-file keys.json

Use the futu-api SDK's WebSocket transport, or a raw client like tokio-tungstenite.

Handshake: HTTP upgrade carries ?token= or Authorization: Bearer. Missing qot:read scope → HTTP 403 with JSON error.

Per-message scope gate (v1.0+): every message is checked against the proto_id's required scope; failures are dropped silently (no response). Clients experience this as "request timeout / subscription not taking effect".

proto_id scope
1xxx system none
3xxx quote + subscribe qot:read
2005 / 2202 / 2205 / 2237 trade write trade:real
Other 2xxx account read + subscribe acc:read

Troubleshooting

Symptom Cause
Handshake 401 Bearer / ?token typo, or key not in keys.json
Handshake 403 Scope lacks qot:read
Subscribe OK but no pushes Has qot:read but not acc:read (trade pushes filtered out)
Some commands don't respond Per-message scope gate dropped them (check audit log)
key revoked Key removed via remove-key + SIGHUP

Audit + observability

  • Both successful and failed handshakes are written to the audit JSONL (iface=ws)
  • Filtered pushes increment futu_ws_filtered_pushes_total{required_scope, key_id}
  • Handshake auth events increment futu_auth_events_total{iface="ws"}

See Audit & observability for details.