Every WebSocket connection failure we've seen reported, with a recipe to fix it. Start from the close code if you have one; otherwise scan the symptom list.
Before debugging in code, run a raw curl handshake. If this works and your code doesn't, the problem is in your client, not the server.
curl --include --no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
--header "Sec-WebSocket-Version: 13" \
--header "X-API-Key: $PARLAY_API_KEY" \
"https://parlay-api.com/v1/ws/odds/baseball_mlb"
Expected first line: HTTP/1.1 101 Switching Protocols. If you get a 4xx instead, see the corresponding row below.
Cause. The key you sent doesn't exist, was revoked, or was rolled.
Fix.
curl -H "X-API-Key: $KEY" https://parlay-api.com/v1/usage. Anything other than 200 means the key is bad.Cause. Your key is on Free, Starter, or Pro tier. WebSocket is gated to Business / Enterprise / Scale.
Fix.
Cause. Too many WebSocket / SSE connections open under one API key.
Fix.
Cause. No API key found in any of the three accepted forms (query param, X-API-Key header, Sec-WebSocket-Protocol subprotocol).
Fix.
wss://parlay-api.com/v1/ws/odds/baseball_mlb?apiKey=YOUR_KEY.headers: { "X-API-Key": KEY } as the second arg to new WebSocket(...).additional_headers={"X-API-Key": KEY} in websockets.connect().Cause. Network-level disconnect: TCP RST, NAT timeout, mobile carrier handoff. This is normal in any long-lived WebSocket setup.
Fix.
Cause. We're restarting (deploys, planned maintenance, worker recycle).
Fix. Wait 5 s, reconnect. Deploys complete in 30–60 s; you'll be back online by your second retry. Subscribe to status to be notified of planned windows.
Cause. Either a server-side exception, or a ping/pong timeout (we send transport pings; if the client doesn't respond for 30 s we treat it as dead).
Fix.
ws, Python websockets, Go gorilla all do by default).Almost always means the API key wasn't recognized at the WAF / edge layer. Check:
https://parlay-api.com (correct) vs http:// or an old domain?X-API-Key header in transit?You hit a non-WebSocket endpoint with WebSocket headers. Double-check the URL path — common typo is /v1/odds/... (REST) vs /v1/ws/odds/... (WebSocket).
Same — URL typo. The four valid URL prefixes are /ws/odds/, /ws/live/, /v1/ws/odds/, /v1/ws/live/. Anything else 404s.
odds_update frames"This is usually fine and not a bug. The collector only pushes when prices change. In quiet windows you can go 60+ seconds with nothing on the wire.
Quick check:
connected frame? If not, you're not authenticated. Re-check the handshake.initial_state frame with count > 0? If count == 0, the sport has no events in scope right now (e.g. WNBA at 3am Eastern). Pick a sport with live coverage.commence_time in the snapshot — if no events tip in the next 24 h, the stream should be quiet.Symptoms: connection establishes fine, but frames arrive in irregular bursts (5–30 s gaps then a flood) instead of streaming smoothly.
Likely causes (in order of frequency):
parlay-api.com to your inspection bypass list.parlay-api.com is in the tunnel set if you're behind a corporate VPN.Don't open WebSocket connections inside Lambda function handlers — they get torn down when the function returns. Two viable patterns:
Known Safari behavior on iOS / macOS — it pauses background tabs aggressively. Your client should reconnect on tab visibility change:
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && ws.readyState !== WebSocket.OPEN) {
ws = new WebSocket(URL);
// re-wire onmessage / onclose / onerror
}
});
That's the WebSocket frame display, not an error. Click the WS request → Messages tab to see actual frames. The "Status: Pending" just means the connection is open (and pending close).
WebSocket bypasses CORS by spec — you should never see a CORS error on the upgrade itself. If you do, it's actually the page-load that's CORS-blocked, not the WebSocket. Make sure the JS file calling new WebSocket() loaded successfully first.
If none of the above helps, please include the following at /support so we don't have to ping-pong:
Median response time on Business+ tickets is under an hour during US business hours.