Streaming (SSE)
GET /v1/stream/contests?series={series} is a long-lived Server-Sent Events stream. Authenticate with the same API key as REST.
Connect
Section titled “Connect”curl -N -H "Authorization: Bearer $COTES_API_KEY" \ -H "Accept: text/event-stream" \ "https://api.cotes.ai/v1/stream/contests?series=KXBTC15M"Browsers cannot set Authorization on the built-in EventSource API. Use fetch with a stream reader, or proxy the stream from your backend.
Event types
Section titled “Event types”Each frame looks like:
event: <name>id: <opaque>data: <json>| Event | data payload |
|---|---|
contest.opened |
Contest (live contest when first seen or a new ticker opens) |
signal.emitted |
SignalEmittedPayload (signal + parent contest) |
contest.settled |
Contest (settled snapshot) |
unauthorized |
Problem detail — terminal, see below |
On connect, you may receive the current live contest and its existing Cotes AI signals before new events arrive.
Keepalive
Section titled “Keepalive”When no event has been sent for 15 seconds the server sends an SSE comment line:
: pingIt is not an event: EventSource and most SSE parsers drop it. If your client reads raw bytes, skip lines that start with :. Treat 45 seconds without any bytes, ping included, as a dead connection and reconnect.
Reconnecting
Section titled “Reconnecting”Send the last id you received in a Last-Event-ID header when you reconnect. The stream then skips what you already have: for the same contest you get only the signals raised after that id, and a contest.settled you missed while away. Without the header, the replay starts again from contest.opened and repeats the contest’s signals, so de-duplicate on id (each frame’s id is unique and stable).
Back off between reconnects: start at 1 second, double up to 30 seconds, and add jitter.
Python consumer
Section titled “Python consumer”A minimal client that parses frames, skips keepalive comments and resumes with Last-Event-ID:
import json, os, random, timeimport requests
URL = "https://api.cotes.ai/v1/stream/contests"HEADERS = {"Authorization": f"Bearer {os.environ['COTES_API_KEY']}", "Accept": "text/event-stream"}
def stream(series="KXBTC15M"): last_id, delay = None, 1 while True: headers = dict(HEADERS, **({"Last-Event-ID": last_id} if last_id else {})) try: with requests.get(URL, params={"series": series}, headers=headers, stream=True, timeout=(10, 45)) as r: r.raise_for_status() delay = 1 event, data = None, [] for line in r.iter_lines(decode_unicode=True): if line.startswith(":"): continue # keepalive ping if line.startswith("event:"): event = line[6:].strip() elif line.startswith("id:"): last_id = line[3:].strip() elif line.startswith("data:"): data.append(line[5:].strip()) elif line == "" and event: payload = json.loads("\n".join(data)) if event == "unauthorized": raise SystemExit(payload["detail"]) # terminal yield event, payload event, data = None, [] except requests.RequestException: time.sleep(delay + random.random()) delay = min(delay * 2, 30)
for event, payload in stream(): if event == "signal.emitted": s = payload["signal"] print(s["side"], s["confidence"], s["entry_price_cents"], s["edge_cents"])The read timeout of 45 seconds matches the keepalive rule above: if no bytes arrive in that time, the request fails and the loop reconnects with the last id it saw.
Open stream limit
Section titled “Open stream limit”Each API key may hold 5 open streams at once, and each client IP 10. One more returns 429 with type https://api.cotes.ai/problems/too-many-streams and Retry-After: 5. Close a stream before opening another; one stream per series is enough for every consumer in a process.
Losing authorization mid-stream
Section titled “Losing authorization mid-stream”Your key and your account’s entitlement are re-checked about every 30 seconds while the stream is open, not just when it opens. Revoke a key or let a subscription lapse while a stream is running and the feed ends with a final unauthorized event:
event: unauthorizeddata: {"type":"https://api.cotes.ai/problems/unauthorized","title":"Unauthorized","detail":"API key revoked or account no longer entitled."}The event exists so the close is distinguishable from a dropped connection. Treat it as terminal — reconnecting fails the same way until the key or the subscription is sorted out, so a client that blindly retries will spin.