Terra API Streaming Best Practices
Guidelines for building on the Terra API Streaming (Real-Time) API, which delivers live, roughly per-second metrics from wearables. This skill carries the architecture and the protocol gotchas inline; platform setup and the full consumer protocol live in references/.
From the terminal
Account configuration lives in the Terra dashboard, which an agent cannot click. The terra CLI does the same from a terminal. The streaming websocket surface itself has no CLI commands, and the CLI does not mint the short-lived token a streaming client authenticates with: that is minted by your backend, per session, through the flow this skill describes. What the CLI covers is the layer under that: the long-lived credential your backend holds in order to mint them, and whether the user being streamed for exists at all.
A stream that will not open is usually a credential without the right scope, a user id with no connection behind it, or a provider that was never enabled in this environment. All three are read-only checks, so run them before debugging the socket:
terra environments list --json dev_id,name
terra data-tokens list --env <dev-id> --json token_id,name,scopes,expires_at,last_used_at,revoked_at
terra users list --env <dev-id> --user-id <uuid> --json user_id,provider,active
terra unified-api sources list --env <dev-id>
data-tokens list answers the credential question without printing a secret: it shows which tokens exist, what each is scoped for, and whether one has expired or been revoked. Do not mint a token to find out whether a token works, and do not reach for terra environments api-key retrieve --reveal here: it prints the environment's API key and webhook signing secret, neither of which tells you anything about a socket, and in CI both land in the job log.
Minting is a setup step rather than a diagnostic. When the list genuinely shows no usable token, terra data-tokens create --env <dev-id> --name streaming --scopes auth:write --reveal returns the bearer once, and several tokens coexist per environment so the old one keeps working until you revoke it.
Install it with brew install tryterra/tap/terra on macOS or npm install -g @tryterra/cli elsewhere. The terra-cli skill carries the guardrails (--reveal on anything returning a credential, --yes on anything destructive), the exit codes, and a playbook per task. It administers the integration; it does not replace the API calls this skill describes.
Streaming vs Health & Fitness
The Streaming API is for realtime, sub-second-to-per-second signals only. The RT SDKs accept seventeen DataTypes values, covering cardiac (heart rate, HRV, RR intervals, ECG), movement (steps, cadence, distance, speed, floors climbed, activity), cycling (power, bike cadence), motion sensors (acceleration, gyroscope), energy (calories, MET), and location. See references/data-types.md for the full enum. Anything with a longer span – workouts, sleep, daily totals, body, nutrition – belongs to the Unified API, not here. If you need a completed workout summary rather than a live feed, you are on the wrong API.
Devices only appear on the stream when they actually broadcast over BLE, ANT+, or a supported custom Bluetooth protocol (heart-rate straps like the Polar H10 or Wahoo TICKR, and some watches). No broadcast means no stream.
Requesting a data type is not the same as receiving it. Terra does not gate or filter by device: the broker passes payloads through opaquely, so what arrives is decided entirely by the wearable's own broadcast profile. Most heart-rate straps are cardiac only and will never produce STEPS or LOCATION however the SDK is configured. Render per-signal state and degrade gracefully rather than treating a missing data type as an error. See references/data-types.md.
Architecture: producer, broker, consumer
Realtime streaming has four parts and you build three connections between them. See getting-started.
- Wearable – the strap, watch, or sensor, broadcasting over BLE or ANT+.
- Producer – your mobile app, running a Terra Real-Time (RT) SDK. It receives the wearable's data and forwards it to the Terra API.
- Terra API WebSocket broker – the server that routes the live stream. This is Terra API infrastructure; you never host it.
- Consumer – your backend, which connects to the broker and receives the stream.
The three connections you build:
- Wearable to app: the user pairs their wearable to your app over Bluetooth/ANT+ using an RT SDK.
- App to broker: your app opens a producer connection and forwards the wearable's data.
- Broker to backend: your backend opens a consumer connection and receives the data live.
You identify a user by your own reference_id. The Terra API mints a Terra user ID for that user (no auth widget needed); that ID is what the token endpoints take and what arrives as the uid field on every payload.
Tokens
Every websocket connection authenticates with a short-lived token minted by your backend from your Dev ID and API key. All three tokens are single-use – the server deletes each one after a successful IDENTIFY, so every reconnect needs a freshly minted token. Never ship your API key into the app; mint tokens server-side and hand them off.
| Token | Endpoint | Used by | IDENTIFY type |
|---|---|---|---|
| Phone-registration | POST https://api.tryterra.co/v2/auth/generateAuthToken |
RT SDK initConnection (registers the phone as a producer) |
n/a (SDK-managed) |
| Producer | POST https://ws.tryterra.co/auth/user?id=<terra_user_id> |
producer connection sending data | 0 (USER) |
| Consumer / developer | POST https://ws.tryterra.co/auth/developer |
your backend consumer | 1 (DEVELOPER) |
Note the hosts: only generateAuthToken lives on the main API (api.tryterra.co). The producer and consumer token endpoints are served over HTTPS by the websocket host (ws.tryterra.co) and do not exist on api.tryterra.co. For the exact request/response schemas, fetch the REST endpoints reference when building the request.
The phone-registration token is single-use and expires 3 minutes after minting (returned as expires_in), so mint it just-in-time – when the app is about to call initConnection, not at app startup or ahead of a queue. The producer endpoint needs the Terra user ID in the id query parameter; retrieve it from the SDK's getUserId.
The websocket
There is one endpoint for both roles: wss://ws.tryterra.co/connect. The IDENTIFY type field (0 producer, 1 consumer) decides which role the connection plays. The SDKs open and drive the producer connection for you; you write the consumer connection by hand against the walkthrough in references/consumer-protocol.md.
The protocol is opcode-framed: HELLO and heartbeats, then IDENTIFY and READY, then DISPATCH data payloads, with REPLAY for backfill (SUBMIT is producer-side and SDK-abstracted). For the full opcode table and exact JSON payload shapes, fetch terra-greater-than-your-backend.md when writing the frames.
Protocol gotchas
These are the things that bite. The step-by-step consumer walkthrough is in references/consumer-protocol.md.
- IDENTIFY within 15 seconds of connecting or the server closes with 4000.
- Tokens are deleted after a successful IDENTIFY. A dropped connection cannot reuse its token; mint a fresh one before reconnecting.
- Heartbeats. HELLO carries
heartbeat_interval(ms). Send the first heartbeat afterheartbeat_interval * jitter(jitter random in 0..1), then at most once per interval. If you get no HEARTBEAT_ACK, close and reconnect. If the server sees no heartbeat within the window it closes with 4005. - Close codes split into two groups. 4000, 4003, 4004, and 1003 signal a client bug – fix the client, do not retry-loop, since a blind reconnect just loops on the same error. 4001 means mint a fresh token. 4002 means the server's consumer session cap rejected the connection – do not architect around any guaranteed number of concurrent consumers; on 4002, close an existing session or back off rather than retry-looping.
seqis monotonic but sparse. Gaps between consecutive sequence numbers are normal and do not mean lost data. Useseqonly to order DISPATCHes and as theafterbound for replay.- REPLAY takes exclusive bounds; send both.
afterandbeforeare both required – a REPLAY that omitsbeforereturns no messages. On reconnect, setafterto the lastseqyou processed andbeforeto theseqof the first live DISPATCH to backfill exactly the gap. - Replay lags a few seconds. A payload becomes replayable a few seconds after it was delivered live. If a REPLAY returns fewer messages than expected, wait a moment and request again.
- Test without hardware. From the Streaming page of the Terra dashboard, create a test user; the Terra API streams synthetic live data through the real API so you can validate a consumer end to end before touching a device.
- Re-init the RT SDK on every app open or foreground. Producer registration does not survive backgrounding.
- Apple Watch records at reduced frequency outside a workout session. Start a workout session on the watch to capture data at the highest frequency.
References
Read the reference for the surface you are building.
- references/consumer-protocol.md – read this before writing the backend consumer. The handshake walkthrough (HELLO, heartbeats, IDENTIFY, READY, DISPATCH), DISPATCH field semantics, the REPLAY backfill recipe, and close-code handling, with pointers to the live doc for exact payload shapes.
- references/data-types.md - read when choosing what to request from the RT SDK, or when a stream is silent. The full
DataTypesandConnectionsenums, why requesting is not receiving, and the Wear OS exercise-prefixed type labels. - references/ios.md – read when the producer app is native iOS or you are wiring an Apple Watch. Apple Developer Program membership is required.
- references/android.md – read when the producer app is native Android, including ANT+ and programmatic device scans.
- references/react-native.md – read when the producer app is React Native.
- references/flutter.md – read when the producer app is Flutter (note the
startRealtimeToAppvsstartRealtimeToServersplit). - references/wear-os.md – read when streaming from a Wear OS watch paired to an Android phone.
Full docs: docs.tryterra.co/streaming-api (append .md to any docs URL for a markdown version). If the terra-docs MCP server (https://docs.tryterra.co/~gitbook/mcp) is connected, use its tools to search and fetch the docs instead.
Decisions that are yours to make
The docs deliberately leave these open; pick what fits your app rather than assuming a default:
- Which data types to stream. You pass the set of types to the SDK; stream only what you use.
- Device-scan caching. Flags like
useCacheandshowWidgetIfCacheNotFound(Android) trade a faster reconnect to a known device against always showing the picker. - Local-only vs server streaming. Streaming to your app locally and streaming to the broker are separate choices. Flutter forces the split explicitly:
startRealtimeToApp(local callback only) vsstartRealtimeToServer(broker only). On other platforms the samestartRealtimecall streams to the broker when you pass a token. - Token hand-off. How the app fetches a producer token from your backend (endpoint shape, auth) is up to you.
- Reconnect and backfill strategy. How aggressively you reconnect, whether you replay on every drop, and how you persist the last processed
seqare your call. - Consumer topology. One consumer fanning out internally vs several is your architecture (subject to the session cap).
reference_idscheme. What yourreference_idmaps to in your own system.