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#
- Connect with your key (
x-api-keyheader or?key=). Ask forpermessage-deflateto receive compressed frames — most clients do this with one option. - The server sends
welcomewith your plan limits. - You send
subscribewithin 30 seconds (or pass?sports=in the URL). - The server sends
subscribed, then a chunkedsnapshot, thendeltabatches until you disconnect. - Every 20 seconds the server sends
ping; reply withpong. 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.
seqincreases 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 |