Streams
Consume events over WebSocket or Server-Sent Events.
Streams push events to you the moment FomoData ingests them. Use the WebSocket for bidirectional subscriptions with filters and cursor resume, or Server-Sent Events when a plain HTTP stream is easier. Both deliver the same Event object you get from REST and webhooks.
Event types
| Type | Fires when | data |
|---|---|---|
thesis.created | A thesis is ingested from the authorized upstream (including backfill). | ThesisCreatedData |
profile.updated | Public profile metadata changed on resync. | ProfileUpdatedData |
token.activity | New theses on a token. Derived, at most once a minute per token. | TokenActivityData |
token.thesis_velocity | Every 5 minutes for tokens with theses in the last hour, only when the value changed. | ThesisVelocityData |
Only event types backed by authorized upstream data exist. Derived events carry source: "fomodata".
The event object
{
"object": "event",
"id": "evt_2Nf8QpLx0VbT6mRz4kWc1S",
"type": "thesis.created",
"created_at": "2026-10-07T10:04:12.000Z",
"livemode": false,
"source": "sandbox",
"data": {
"thesis_id": "th_7Hq2LmZx9RkT4bVn0sYc3D",
"profile": {
"id": "fp_4KZq8mXw2T0aN6rB1cYd9E",
"handle": "@sbx_milo",
"display_name": "Milo (sandbox)",
"avatar_url": null
},
"token": {
"id": "tk_9bF2kLmQ0sVx7RtY3nHc1A",
"symbol": "$RUN",
"name": "Run (sandbox)",
"chain": "sandbox",
"address": "0x393e5c8b655042f2409351fc00c2714947537a12"
},
"text": "Sandbox thesis: course looks fast today.",
"created_at": "2026-10-07T10:04:12.000Z",
"public_url": null
}
}id is stable: the same event always has the same id across REST, SSE, WebSocket, webhooks and replays, so you can deduplicate on it. Test events you generate carry test: true.
WebSocket
wss://visits-taking-trademark-corner.trycloudflare.com/v1/streamAuthentication
Send Authorization: Bearer fd_… on the upgrade request (server-side clients), or connect and send an auth message within 10 seconds. Keys are never accepted in the URL.
{ "action": "auth", "api_key": "fd_test_…" }The server greets you once authenticated:
{ "type": "welcome", "connection_id": "conn_…", "livemode": false, "project_id": "proj_…", "heartbeat_seconds": 20 }Subscribe
{
"action": "subscribe",
"id": "race-entries",
"events": ["thesis.created"],
"filters": { "token": "$RUN", "handle": "@milo", "chain": "robinhood" },
"from_cursor": "1042"
}eventsstring[]- Event types to receive. Omit (or send []) for every type.
filtersobjecttoken($SYMBOL, address or tk_ id),handle,chain; each a string or an array (values OR-ed). All optional; dimensions combined with AND.from_cursorstring- Replay events after this cursor (max 1,000, within your plan's history) before live delivery.
idstring- Optional client reference echoed back.
{ "type": "subscribed", "subscription_id": "ssub_…", "events": ["thesis.created"], "filters": { "token": "$RUN" } }Events then arrive as:
{ "type": "event", "subscription_id": "ssub_…", "cursor": "1043", "event": { "object": "event", "id": "evt_…", "type": "thesis.created", … } }Other messages
| Direction | Message |
|---|---|
| client → server | { "action": "unsubscribe", "subscription_id": "ssub_…" } |
| client → server | { "action": "ping" } |
| server → client | { "type": "pong" } |
| server → client | { "type": "heartbeat", "ts": "…" } |
| server → client | { "type": "error", "error": { "type", "code", "message" } } |
Close codes
| Code | Meaning |
|---|---|
4001 | Unauthorized: missing, invalid, revoked or expired key. |
4003 | Forbidden: the key lacks streams:read, the project isn't approved for live data, or it is archived. |
4008 | Authentication timeout (no auth within 10 seconds). |
4029 | Connection limit reached for your plan, the key's rate limit or monthly quota (the error frame says which), or too many failed authentications from your address. |
1013 | Slow consumer: more than 1 MB of events was waiting to be sent. Reconnect and subscribe with from_cursor = your last cursor; nothing is lost. |
1001 | Server restarting (deploy). Reconnect and subscribe with from_cursor. |
1008 | Too many control messages (more than 60 in 10 seconds, or more than 256 KB waiting to be handled). Slow down, then reconnect. |
4001 and 4003 won't succeed on retry; fix the key or project first. For every other close, reconnect with backoff and resume from your last cursor. Authorization is re-checked while a stream is open: revoking or expiring the key, removing streams:read, archiving the project or losing live approval closes open WebSockets with 4001/4003 and ends SSE streams with an error event, usually within seconds and at most about 20 seconds later. A WebSocket connect, and every subscribe with from_cursor (at most 6 per minute per connection), counts as a request toward your rate limit and monthly quota, like SSE and REST.
Resume after a disconnect
Remember the last cursor you processed. On reconnect, subscribe with from_cursor and FomoData replays what you missed (up to 1,000 events within your plan's history), then continues live. Combined with deduplication on event.id, this gives you at-least-once, gap-free consumption.
With the SDK
The SDK handles auth, heartbeats, reconnects and cursor resume for you: after 1013, 1001 or a dropped connection it reconnects with backoff and re-subscribes with from_cursor set to the last event it delivered. 4001 and 4003 stop it and call onError.
import { FomoData } from "@fomodata/sdk";
const fomo = new FomoData({ apiKey: process.env.FOMODATA_API_KEY });
fomo.streams.subscribe({
event: "thesis.created",
token: "$RUN",
onEvent(event) {
console.log(event);
}
});Server-Sent Events
GET/v1/events/streamstreams:readtypesstring- Comma-separated event types, e.g.
thesis.created,token.activity. token · handle · chainstring- Same filters as the WebSocket.
from_cursorstring- Resume after this cursor (a frame's
id). TheLast-Event-IDheader takes precedence.
curl -N 'https://fomodata.dev/v1/events/stream?types=thesis.created&token=$RUN' \
-H "Authorization: Bearer $FOMODATA_API_KEY"Authenticate with the Authorization header. Each frame's id is the event cursor; reconnect with Last-Event-ID to resume. A : ping comment arrives every 15 seconds to keep proxies from closing the connection.
Event history over REST
Need to backfill or audit? The same events are queryable.
GET/v1/eventsevents:readGET/v1/events/{id}events:readLimits and metering
- Concurrent connections per project are capped by your plan's stream_connections. Extra connections close with
4029(WebSocket) or429 STREAM_CONNECTION_LIMIT(SSE). - Each delivered event counts as one stream message, and each connect as one stream connection, in your usage.