WebSocket protocol

wss://pinnfeed.io/v1/stream

One connection carries everything you subscribe to. All frames are JSON text. Available on Hobby, Builder, Pro and Enterprise.

Connection lifecycle#

  1. Connect with your key (x-api-key header or ?key=). Ask for permessage-deflate to receive compressed frames — most clients do this with one option.
  2. The server sends welcome with your plan limits.
  3. You send subscribe within 30 seconds (or pass ?sports= in the URL).
  4. The server sends subscribed, then a chunked snapshot, then delta batches until you disconnect.
  5. Every 20 seconds the server sends ping; reply with pong. Connections silent for 60 seconds are closed.

Client → server messages#

subscribe#

{
  "type": "subscribe",
  "sports": ["soccer", "tennis"],
  "leagues": [1980, 2627],
  "events": [1640278226],
  "markets": ["moneyline", "spread", "total"],
  "live": true,
  "main": false,
  "specials": true,
  "snapshot": true,
  "format": "normalized"
}
Field Type Default Meaning
sports array of slugs or ids all sports in your plan Required on plans with a sports limit. Slugs: soccer, tennis, basketball, hockey, american-football, baseball, rugby, mma, boxing, volleyball-handball, esports, golf, cricket.
leagues array of ids all Only these league ids.
events array of ids all Only these event ids.
markets array all Any of moneyline, spread, total, team_total.
live true / false / omitted both true = in-play only, false = prematch only.
main boolean false Only the main line of moneyline, spread and total (forced on Free and Hobby).
specials boolean true Include props and futures (Builder and above).
snapshot boolean true Send the current state before deltas.
format normalized / raw normalized raw forwards upstream frames untouched (Pro and Enterprise).

Sending subscribe again replaces the previous subscription and sends a fresh snapshot. {"type":"unsubscribe"} stops all data.

pong / ping#

Reply to the server ping with {"type":"pong"}. You can also send {"type":"ping"} and the server answers {"type":"pong","ts":...}.

Server → client messages#

welcome#

{ "type": "welcome", "version": "v1", "plan": "builder",
  "limits": { "connections": 3, "sports": [1,2,3,4,5,6,7,8,9,10,11,12,13], "mainMarketsOnly": false, "specials": true, "raw": false },
  "compression": true }

snapshot#

The current state matching your filters, split into parts of up to 400 events:

{ "type": "snapshot", "seq": 1022, "part": 1, "parts": 11, "final": false,
  "events": [ { "id": 1640278226, "sport": "soccer", "home": "FC Prishtina", "away": "Malisheva", "live": true } ],
  "markets": [ { "id": "1640278226:s;0;m", "eventId": 1640278226, "type": "moneyline", "outcomes": [] } ] }

Wait for final: true before treating your book as complete. Full objects are described in the schema reference.

delta#

One batch per upstream update, containing only what changed for your filters:

{ "type": "delta", "seq": 1023, "ts": 1762679412093,
  "changes": [
    { "kind": "market", "op": "upsert", "market": { "id": "1640278226:s;0;m", "...": "..." } },
    { "kind": "market", "op": "remove", "id": "1640278226:s;0;ou;2.5", "eventId": 1640278226 },
    { "kind": "event",  "op": "upsert", "event":  { "id": 1640278226, "...": "..." } },
    { "kind": "event",  "op": "remove", "id": 1640270607 }
  ] }
  • upsert carries the complete object — replace what you have.
  • remove carries only the id. When an event is removed, drop all of its markets too.
  • seq increases by one per batch across the server; use it for logging, not for gap detection (filters skip batches you are not subscribed to).

error#

{ "type": "error", "code": "sport_not_in_plan", "message": "Not in your plan: sport ids 4. ..." }

Errors on subscribe do not close the connection. See errors for all codes.

Close codes#

Code Reason What to do
1001 server_shutdown Deploy or restart. Reconnect with backoff.
4001 key_revoked The key was revoked. Use another key.
4002 no_subscription No subscribe within 30 s.
4008 slow_consumer You read too slowly and more than 16 MB queued. Process messages faster or narrow your filters.

Before the upgrade, an invalid request is rejected with an HTTP status and a JSON body: 401 invalid_key, 403 plan_lacks_ws, 429 connection_limit.

Reconnecting#

Reconnect with exponential backoff (1 s, 2 s, 4 s … capped at 30 s, plus jitter), resubscribe and rebuild your book from the new snapshot.

Compression#

Clients that offer permessage-deflate get compressed frames (threshold 1 KB). Snapshots shrink 5–10×, so enable it unless CPU is very tight.

Client Enable compression
Python websockets on by default (compression="deflate")
Node ws new WebSocket(url, { perMessageDeflate: true })
Browsers automatic