Skip to Content
API-ReferenzWebSocket-Stream

WebSocket-Stream

wss://ws.sharpapi.io — Echtzeit-Aktualisierungen von Quoten und Opportunities über WebSocket.

Erfordert das WebSocket Add-on (99 $/Monat) auf jeder kostenpflichtigen Stufe oder Enterprise (inklusive). Die kostenlose Stufe unterstützt kein Streaming.

Eine maschinenlesbare AsyncAPI-3.0-Beschreibung dieses Endpoints — Channels, Nachrichten, Schemas und Bindings — wird unter /asyncapi.yaml veröffentlicht. Verwenden Sie sie für die SDK-Codegenerierung oder zur Steuerung von AsyncAPI-Tools wie Studio .

Warum WebSocket?Permalink for this section

WebSocket bietet eine persistente Vollduplex-Verbindung. Im Vergleich zu SSE:

FunktionSSE (/api/v1/stream)WebSocket (ws.sharpapi.io)
RichtungNur Server → ClientBidirektional
WiederverbindungAutomatisch (Last-Event-ID)Vom Client verwaltet
FilterEinmalig über Query-ParameterJederzeit über subscribe-Nachricht aktualisierbar
ProtokollHTTP/1.1-StreamingWebSocket (RFC 6455)
Browser-UnterstützungNatives EventSourceNatives WebSocket

Beide Protokolle liefern dieselben Daten mit derselben Latenz. Wählen Sie WebSocket, wenn Sie Filter ändern müssen, ohne eine neue Verbindung herzustellen.

AuthentifizierungPermalink for this section

Übergeben Sie Ihren API key als Query-Parameter in der Verbindungs-URL:

wss://ws.sharpapi.io?api_key=sk_live_your_key

Sie können auch initiale Filter und Channel-Abonnements als Query-Parameter übergeben:

wss://ws.sharpapi.io?api_key=sk_live_your_key&channels=ev,odds&sport=basketball&sportsbook=draftkings,fanduel&league=nba

Query-ParameterPermalink for this section

ParameterTypStandardBeschreibung
api_keystring—Erforderlich. Ihr API key
channelsstringalleAbonnement bestimmter Datenchannels, kommagetrennt. Gültige Werte: ev, arbitrage, middles, low_hold, odds. Weglassen, um alle für die Stufe zulässigen Daten zu erhalten.
sportstringalleFilter nach Sportart(en), kommagetrennt (z. B. basketball, football, ice_hockey)
sportsbookstringtier-zulässigFilter nach Sportsbook(s), kommagetrennt
leaguestringalleFilter nach Liga(en), kommagetrennt
marketstringalleFilter nach Markttyp(en), kommagetrennt (z. B. moneyline, point_spread, total_points, player_points)
event_idstringalleFilter nach bestimmten Event-ID(s), kommagetrennt
min_evnumber2.0Minimaler EV-Prozentsatz für +EV-Opportunities
min_profitnumber0.5Minimaler Gewinnprozentsatz für Arbitrage- und Low-Hold-Opportunities
min_oddsnumber—Quoten nach minimalem amerikanischem Quotenwert filtern (z. B. -200)
max_oddsnumber—Quoten nach maximalem amerikanischem Quotenwert filtern (z. B. 500)
statestring—US-Bundesstaatscode für Sportsbook-Deeplinks in Quoten- und Opportunity-Events (z. B. nj, ny, il). Stellt sicher, dass deep_link-URLs zur korrekten bundesstaatsspezifischen Sportsbook-Domain weiterleiten.
from_seqstring—Best-Effort-Replay nach diesem opaken global_seq-Checkpoint versuchen. Siehe Wiederverbindung mit Replay.

Verwenden Sie Channels, um die Payload-Größe zu reduzieren. Ohne channels sendet der Server alle Opportunity-Typen sowie den vollständigen Quoten-Dump. Wenn Sie nur Low-Hold-Daten benötigen, verbinden Sie sich mit channels=low_hold, um EV, Arbitrage, Middles und Rohquoten vollständig zu überspringen.

VerbindungslebenszyklusPermalink for this section

Client Server | | |--- WS Upgrade ?api_key=xxx&channels=ev,odds →| | | Auth + acquire stream slot |← connected ----------------------------------| Welcome (tier, features, channels) |← subscribed ---------------------------------| Filter confirmation |← opportunities_snapshot (ev) ----------------| EV opportunities |← initial (draftkings) -----------------------| Odds per sportsbook |← initial (fanduel) --------------------------| (chunked by book) |← snapshot:complete --------------------------| All initial data sent | | |← odds:update --------------------------------| Incremental odds update |← ev:detected --------------------------------| +EV opportunity (new or updated) |← heartbeat ----------------------------------| Keep-alive (every 30s) | | |--- { type: "ping" } → | |← pong ---------------------------------------| | | |--- { type: "subscribe", channels, filters } →| Update channels/filters |← subscribed ---------------------------------| New subscription confirmed | | |--- close ----------------------------------→| Normal close (1000)

NachrichtenprotokollPermalink for this section

Client → ServerPermalink for this section

subscribe — Channels und Filter setzen oder aktualisieren. Wird beim Verbinden automatisch gesendet, wenn als Query-Parameter übergeben.

{ "type": "subscribe", "channels": ["ev", "odds"], "filters": { "sports": ["basketball"], "sportsbooks": ["draftkings", "fanduel"], "leagues": ["nba"], "markets": ["moneyline", "player_points"], "eventIds": ["32825-35775-2026-02-08"], "min_ev": 3.0, "min_profit": 1.5 } }
FeldTypBeschreibung
channelsstring[]Optional. Datenchannels, die abonniert werden sollen: ev, arbitrage, middles, low_hold, odds. Weglassen, um die aktuellen Channels beizubehalten.
filters.sportsstring[]Optional. Filter nach Sportart(en): basketball, football, ice_hockey, baseball, soccer usw.
filters.sportsbooksstring[]Optional. Filter nach Sportsbook(s).
filters.leaguesstring[]Optional. Filter nach Liga(en).
filters.marketsstring[]Optional. Filter nach Markttyp(en).
filters.eventIdsstring[]Optional. Filter nach bestimmten Event-ID(s).
filters.min_evnumberOptional. Minimaler EV-Prozentsatz-Schwellenwert (Standard 2.0).
filters.min_profitnumberOptional. Minimaler Gewinnprozentsatz für Arbitrage/Low-Hold (Standard 0.5).

ping — Keepalive. Alle 25 Sekunden senden, um Timeouts zu verhindern.

{ "type": "ping" }

resyncPermalink for this section

{ "type": "resync", "channels": ["odds"] }

Sendet den initialen Snapshot für Channels, die Sie bereits abonniert haben, über den offenen Socket neu. Abonnements und Filter bleiben unverändert, sodass keine Updates in einer Lücke zwischen Abbestellen und erneutem Abonnieren verloren gehen.

  • Bestätigt mit resync:started, gefolgt von der gleichen Snapshot-Sequenz, die ein neues Abonnement erzeugt.
  • Nur bereits abonnierte Channels werden akzeptiert — dies ist keine Hintertür für ein Abonnement. Die Angabe eines nicht abonnierten Channels lehnt die gesamte Nachricht mit channel_not_subscribed ab.
  • Rate-limitiert auf eine pro 10 Sekunden pro Verbindung — eine Ablehnung ist resync_rate_limited mit retry_after_ms, und eine überlappende Anfrage ist resync_in_progress. Ein Snapshot-Dump ist aufwendig; ein unbegrenzter clientseitig getriggerter Dump wäre ein selbstverschuldeter Denial of Service.
  • Die Unterstützung wird als features.client_resync im connected-Ack beworben — prüfen Sie dort, anstatt manuell zu testen.

resync_required ist das separate Server-zu-Client-Signal, das unter Vollständige Resynchronisierung beschrieben ist; senden Sie es niemals an den Server zurück.

Server → ClientPermalink for this section

connectedPermalink for this section

Wird unmittelbar nach erfolgreicher Authentifizierung gesendet.

{ "type": "connected", "seq": 12847, "message": "Welcome to SharpAPI real-time odds stream", "stream_id": "ws_mle3husw_ezoyvp", "tier": "pro", "features": { "ev": true, "arbitrage": true, "middles": true, "low_hold": true }, "channels": ["ev", "odds"], "global_seq": "12847", "books": { "max": -1, "allowed": null }, "streams": { "max": 1, "active": 1 }, "heartbeat_interval_ms": 30000, "pong_timeout_ms": 120000, "timestamp": "2026-02-08T18:47:17.559Z" }
FeldTypBeschreibung
seqintegerÄltere Integer-Darstellung des prozessglobalen Checkpoints. Nicht jeder Kontroll- oder Snapshot-Frame enthält ihn.
stream_idstringEindeutige Verbindungskennung
tierstringIhre Abonnementstufe
featuresobjectWelche Opportunity-Typen Ihre Stufe unterstützt
channelsstring[] | nullAktive Channel-Abonnements oder null, wenn alle für die Stufe zulässigen Daten empfangen werden
global_seqstringJavaScript-sicherer Dezimal-String-Checkpoint. Behandeln Sie ihn als opak und speichern Sie ihn ohne numerische Konvertierung.
resumedbooleanBei einem from_seq-Versuch, ob der Replay akzeptiert wurde. Eine Ablehnung enthält fallback_reason.
fallback_reasonstringVorhanden, wenn ein angefordertes Resume abgelehnt wird; die Verbindung erhält dann einen vollständigen autoritativen Snapshot.
streamsobjectIhre verbindungsübergreifende Gleichzeitigkeitsgrenze pro Key. max ist maßgebend — es ist derselbe Wert, den der Server durchsetzt. active ist informativ: Es wird pro Serverprozess gezählt, während die Grenze flottenweit gilt, sodass active < max nicht beweist, dass Ihre nächste Verbindung keine bestehende verdrängt. Behandeln Sie 4001 unabhängig davon.
heartbeat_interval_msintegerTakt des heartbeat-Anwendungsframes in Millisekunden. Richten Sie Ihren Liveness-Watchdog nach diesem Wert aus, anstatt einen festen Wert zu verwenden — siehe heartbeat.
pong_timeout_msintegerDie Lese-Deadline des Servers. Wenn kein PONG innerhalb dieses Fensters eintrifft, wird die Verbindung geschlossen. Dies ist eine harte Deadline, im Gegensatz zum Best-Effort-Heartbeat-Frame.
books.maxintegerMaximale Sportsbooks, die für Ihre Stufe zulässig sind (-1 = unbegrenzt)
books.allowedstring[] | nullSpezifisch erlaubte Sportsbooks oder null für alle

Wird ein angefordertes Resume abgelehnt, meldet derselbe Frame den expliziten Ausgang:

{ "type": "connected", "global_seq": "12847", "resumed": false, "fallback_reason": "seq_too_old" }

subscribedPermalink for this section

Bestätigt Ihre aktiven Channels und Filter.

{ "type": "subscribed", "channels": ["ev", "odds"], "sports": ["basketball"], "sportsbooks": ["draftkings", "fanduel"], "leagues": ["nba"], "markets": null, "eventIds": null, "min_ev": 3.0, "min_profit": 1.5, "timestamp": "2026-02-08T18:47:17.561Z" }

opportunities_snapshotPermalink for this section

Snapshot von Opportunities für einen einzelnen Channel-Typ. Wird einmal pro abonniertem Opportunity-Channel während des initialen Datenladens gesendet. Enthält nur den von Ihnen abonnierten Opportunity-Typ.

{ "type": "opportunities_snapshot", "ev": [ { "id": "a1b2c3d4e5f6", "game_id": "nba_indianapacers_torontoraptors_2026-02-08", "ev_percentage": 4.35, "odds_american": -110, "odds_decimal": 1.909, "no_vig_odds": -101, "selection": "Tyrese Haliburton Over 22.5", "market": "player_points", "line": 22.5, "sportsbook": "draftkings", "game": "Indiana Pacers @ Toronto Raptors", "sport": "basketball", "league": "nba", "home_team": "Toronto Raptors", "away_team": "Indiana Pacers", "start_time": "2026-02-08T19:00:00.000Z", "is_live": false, "confidence_score": 72, "kelly_percent": 3.8, "book_count": 4, "detected_at": "2026-02-08T18:47:20.000Z" } ], "timestamp": "2026-02-08T18:47:17.700Z" }

Der Schlüssel auf oberster Ebene entspricht dem Channel-Typ: ev, arbitrage, middles oder low_hold. Jede Snapshot-Nachricht enthält nur einen Typ. Große Snapshots werden automatisch in Chunks aufgeteilt — in diesem Fall enthalten die Nachrichten die Felder chunk und totalChunks.

Alle Opportunity-Felder verwenden die snake_case-Benennung (z. B. event_id, market_type, profit_percent, detected_at). Dies gilt einheitlich für alle Channels, Nachrichtentypen und Protokolle (REST, SSE und WebSocket).

initialPermalink for this section

Quoten-Snapshot pro Sportsbook. Wird einmal pro Sportsbook gesendet, wenn der odds-Channel abonniert ist. Erfordert den odds-Channel.

{ "type": "initial", "source": "draftkings", "data": [ /* NormalizedOdds[] */ ], "count": 1500, "timestamp": "2026-02-08T18:47:17.800Z" }

Quoten werden nach Sportsbook in Chunks aufgeteilt — Sie erhalten eine initial-Nachricht pro Buch. Große Bücher können auf mehrere Nachrichten aufgeteilt werden (jeder Frame ist auf 256KB serialisiert begrenzt). Wenn Sie keine Rohquoten benötigen, lassen Sie den odds-Channel weg, um diesen Schritt vollständig zu überspringen.

snapshot:completePermalink for this section

Signalisiert das Ende eines initialen Snapshots, eines Full-Resync-Fallbacks oder eines erfolgreichen Replays. Nach Erhalt können Ladezustände sicher ausgeblendet werden. Ein vollständiger Snapshot enthält books und total_odds. Wird ein angefordertes Resume abgelehnt, enthält er zusätzlich mode: "full_resync" und denselben fallback_reason, den bereits connected gemeldet hat:

{ "type": "snapshot:complete", "books": ["draftkings", "fanduel", "pinnacle"], "total_odds": 2841, "mode": "full_resync", "fallback_reason": "seq_too_old" }

Ein akzeptierter normaler oder konsolidierter Replay hat eine andere Abschlussform:

{ "type": "snapshot:complete", "mode": "resume", "replayed_count": 127, "skipped_count": 3, "last_seq": "12974", "gap_detected": false }

Ein gewöhnlicher frischer Snapshot lässt mode und fallback_reason weg. Die Replay-Annahme meldet der vorangehende connected-Frame, nicht dieser Abschluss-Frame.

FeldTypBeschreibung
booksstring[]Liste der im initialen Snapshot enthaltenen Sportsbooks
total_oddsintegerGesamtzahl der im vollständigen Snapshot gesendeten Quotenzeilen
modestringresume nach einem Replay oder full_resync nach einem explizit abgelehnten Resume. Ein gewöhnlicher frischer Snapshot kann es weglassen.
fallback_reasonstringWarum der angeforderte Checkpoint nicht wiedergegeben werden konnte. Entspricht dem Grund in connected.
replayed_countintegerNormaler Replay: gesendete gepufferte Frames. Konsolidierter Replay: gesendete aktuelle geänderte Zeilen.
skipped_countintegerDurch das aktive Abonnement und die Filter ausgeschlossene gepufferte Frames. Konsolidierter Replay meldet 0.
last_seqstringServerausgestellte Quittung für den abgeschlossenen Replay-Durchlauf. Speichern Sie ihn erst nach Erhalt dieser Abschlussgrenze.
gap_detectedbooleanBei einem gestarteten Replay bedeutet true, dass der Client den Status abgleichen muss, statt den Abschluss als autoritativ zu behandeln.

odds:updatePermalink for this section

Inkrementelle Quotenaktualisierung von einem einzelnen Sportsbook.

{ "type": "odds:update", "seq": 46, "source": "draftkings", "data": [ /* NormalizedOdds[] */ ], "count": 23, "timestamp": "2026-02-08T18:47:19.123Z" }

odds:removedPermalink for this section

Von einem Sportsbook entfernte Quoten (z. B. Markt heruntergenommen, Event abgeschlossen).

{ "type": "odds:removed", "seq": 47, "source": "draftkings", "ids": ["odd_id_1", "odd_id_2"], "count": 2, "timestamp": "2026-02-08T18:47:19.200Z" }

ev:detectedPermalink for this section

Neue +EV-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Nur Pro-Stufe oder höher.

:detected bedeutet neu oder aktualisiert — Upsert per id. Der WebSocket-Stream sendet eine Opportunity, deren Inhalt sich unter derselben id geändert hat — ein neuer Preis, EV%, faire Wahrscheinlichkeit, Leg-Quoten, is_suspended oder quality_tier —, erneut in der gewöhnlichen ev:detected-, arb:detected-, middles:detected- oder low_hold:detected-Nachricht. Speichern Sie Opportunities nach id und ersetzen Sie die gespeicherte Version durch jeden Eintrag. Lösen Sie Alerts nur für eine id aus, die Sie noch nicht gesehen haben, und vergessen Sie eine id, sobald *:expired sie auflistet. Der SSE-Stream verhält sich seit dem 2026-09-26 genauso.

{ "type": "ev:detected", "seq": 48, "data": [ { "id": "a1b2c3d4e5f6", "game_id": "nba_indianapacers_torontoraptors_2026-02-08", "ev_percentage": 4.35, "odds_american": -110, "odds_decimal": 1.909, "no_vig_odds": -101, "selection": "Tyrese Haliburton Over 22.5", "market": "player_points", "line": 22.5, "sportsbook": "draftkings", "game": "Indiana Pacers @ Toronto Raptors", "sport": "basketball", "league": "nba", "home_team": "Toronto Raptors", "away_team": "Indiana Pacers", "start_time": "2026-02-08T19:00:00.000Z", "is_live": false, "confidence_score": 72, "kelly_percent": 3.8, "book_count": 4, "detected_at": "2026-02-08T18:47:20.000Z" } ], "timestamp": "2026-02-08T18:47:20.000Z" }

ev:expiredPermalink for this section

Zuvor erkannte +EV-Opportunity ist nicht mehr verfügbar.

{ "type": "ev:expired", "seq": 49, "data": { "expired": [ "32825-35775-2026-02-08:draftkings:Tyrese Haliburton Over 22.5" ] }, "timestamp": "2026-02-08T18:47:25.000Z" }

arb:detectedPermalink for this section

Neue Arbitrage-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Nur Hobby-Stufe oder höher.

{ "type": "arb:detected", "seq": 50, "data": [ { "id": "61c501b83ce932d1", "event_id": "nba_indianapacers_torontoraptors_2026-02-08", "event_name": "Indiana Pacers @ Toronto Raptors", "sport": "basketball", "league": "nba", "market_type": "moneyline", "line": null, "profit_percent": 2.8, "implied_total": 97.2, "is_live": false, "legs": [ { "sportsbook": "draftkings", "selection": "Indiana Pacers", "odds_american": 125, "odds_decimal": 2.25, "implied_probability": 0.4444, "stake_percent": 52.8 }, { "sportsbook": "fanduel", "selection": "Toronto Raptors", "odds_american": -110, "odds_decimal": 1.909, "implied_probability": 0.5238, "stake_percent": 47.2 } ], "detected_at": "2026-02-08T18:47:21.000Z" } ], "timestamp": "2026-02-08T18:47:21.000Z" }

arb:expiredPermalink for this section

Zuvor erkannte Arbitrage-Opportunity ist nicht mehr verfügbar.

{ "type": "arb:expired", "seq": 51, "data": { "expired": [ "32825-35775-2026-02-08:moneyline" ] }, "timestamp": "2026-02-08T18:47:26.000Z" }

middles:detectedPermalink for this section

Neue Middle-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Erfordert den middles-Channel.

{ "type": "middles:detected", "seq": 52, "data": [ { "id": "abc123", "event_id": "nba_indianapacers_torontoraptors_2026-02-08", "event_name": "Indiana Pacers @ Toronto Raptors", "sport": "basketball", "league": "nba", "market_type": "player_points", "side1": { "book": "draftkings", "selection": "Over 22.5", "line": 22.5, "odds": { "american": -110, "decimal": 1.909, "probability": 0.5238, "fair_probability": 0.51 }, "stake_percent": 50, "odds_age_seconds": 3.2, "deep_link": null }, "side2": { "book": "fanduel", "selection": "Under 23.5", "line": 23.5, "odds": { "american": -105, "decimal": 1.952, "probability": 0.5122, "fair_probability": 0.49 }, "stake_percent": 50, "odds_age_seconds": 1.8, "deep_link": null }, "middle_size": 1, "middle_numbers": [23], "middle_probability": 0.12, "expected_value": 3.5, "roi_percentage": 4.2, "quality_score": 85, "detected_at": "2026-02-08T18:47:22.000Z" } ], "timestamp": "2026-02-08T18:47:22.000Z" }

middles:expiredPermalink for this section

Zuvor erkannte Middle-Opportunity ist nicht mehr verfügbar.

{ "type": "middles:expired", "seq": 53, "data": { "expired": ["abc123"] }, "timestamp": "2026-02-08T18:47:27.000Z" }

low_hold:detectedPermalink for this section

Neue Low-Hold-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Erfordert den low_hold-Channel.

{ "type": "low_hold:detected", "seq": 54, "data": [ { "id": "def456", "event_id": "nba_indianapacers_torontoraptors_2026-02-08", "event_name": "Indiana Pacers @ Toronto Raptors", "sport": "basketball", "league": "nba", "market_type": "moneyline", "line": null, "home_team": "Toronto Raptors", "away_team": "Indiana Pacers", "start_time": "2026-02-08T19:00:00.000Z", "hold_percentage": 1.2, "is_live": false, "all_books": ["draftkings", "fanduel"], "side1": { "selection": "Indiana Pacers", "books": ["draftkings"], "line": null, "odds": { "american": -108, "decimal": 1.926, "implied_probability": 0.5192, "fair_probability": 0.5096 }, "deep_links": { "draftkings": "https://sportsbook.draftkings.com/event/..." } }, "side2": { "selection": "Toronto Raptors", "books": ["fanduel"], "line": null, "odds": { "american": 110, "decimal": 2.1, "implied_probability": 0.4762, "fair_probability": 0.4904 }, "deep_links": { "fanduel": "https://sportsbook.fanduel.com/event/..." } }, "detected_at": "2026-02-08T18:47:22.000Z" } ], "timestamp": "2026-02-08T18:47:22.000Z" }

low_hold:expiredPermalink for this section

Zuvor erkannte Low-Hold-Opportunity ist nicht mehr verfügbar.

{ "type": "low_hold:expired", "seq": 55, "data": { "expired": ["def456"] }, "timestamp": "2026-02-08T18:47:28.000Z" }

heartbeatPermalink for this section

Ein Liveness-Frame auf Anwendungsebene, der in einem festen Takt an jede authentifizierte Verbindung gesendet wird, unabhängig von der Marktaktivität — er trifft auch auf einem ruhigen nächtlichen Book ein. Das Intervall wird als heartbeat_interval_ms im connected-Ack beworben (heute 30 s).

{ "type": "heartbeat", "seq": 150, "global_seq": "150", "timestamp": "2026-02-08T18:48:17.559Z" }

Er trägt den aktuellen Sequenzwert, was ihn über eine reine Liveness-Prüfung hinaus nützlich macht. Lesen Sie ihn als zweiseitige Prüfung, da es ein einzelner Zähler ist, der von jedem Channel und Book auf der Serverinstanz geteilt wird, mit der Sie verbunden sind:

  • Ein Anstieg bedeutet nicht, dass Ihre Daten fließen. Er bewegt sich, wenn irgendein Book Inhalte ausgibt, sodass er einen Stillstand bei einem einzelnen Book nicht aufdecken kann — ein Book, das schweigt, während die anderen streamen, lässt den Zähler weiter steigen.
  • Ein Stillstand bedeutet nicht immer, dass etwas kaputt ist. Er ist genauso flach auf einem wirklich ruhigen Slate, wie z. B. einem nächtlichen Spiel, in dem sich nichts bewegt.

Behandeln Sie ein flaches global_seq daher nur als Stillstandssignal, wenn Ihre eigenen Zeilen ebenfalls veraltet sind und Sie Aktivität erwarten. Bei einem flachen Zähler allein zu reconnecten, erzeugt einen vollständigen Snapshot auf einem ruhigen Markt — die unten beschriebene Spirale. Für buch-spezifische Liveness: Verfolgen Sie, wann Sie zuletzt eine Zeile für jedes Book erhalten haben, das Ihnen wichtig ist; der WebSocket-heartbeat-Frame enthält keine pro-Book-Zeitstempel.

Dies ist nur ein Stillstandssignal, kein Reconnect-Checkpoint — siehe Wiederverbindung mit Replay für die sichere Quittierung.

Dieser Frame ist Best-Effort, und die zwei 30-Sekunden-Timer haben unterschiedliche Verträge. Der Heartbeat wird mit einem nicht-blockierenden Send ausgeliefert, sodass ein durch Backpressure belasteter Client legitim einen Zyklus verpasst — und die Bedingung, die ihn fallen lässt, ist Ihre eigene Langsamkeit, sodass ein auf einem Intervall gesetzter Watchdog einen vorübergehenden Stillstand in einen Fehlalarm beim Reconnect verwandelt, der einen vollständigen Snapshot neu lädt und den Backpressure verschlimmert.

Der harte Vertrag ist die PONG-Deadline des Protokolls (pong_timeout_ms, heute 120 s): Trifft kein PONG innerhalb dieses Fensters ein, schließt der Server die Verbindung. Das ist eine Lese-Deadline, kein Missed-Beat-Zähler.

Richten Sie einen Heartbeat-Watchdog aus beiden beworbenen Werten aus:

watchdog_ms = min(3 * heartbeat_interval_ms, pong_timeout_ms - heartbeat_interval_ms)

was bei den heutigen Takten 90 s ergibt. Das min ist wichtig: Ein bloßes 3x-faches kann das gesamte Pong-Budget bei einem anderen Takt überschreiten. Die beiden Timer sind unabhängig phasiert — die Lese-Deadline wird zurückgesetzt, wenn Ihr PONG ankommt, der Heartbeat läuft von einem serverweiten Ticker — sodass dies begrenzt, wie lange Sie warten, aber nicht garantiert, dass Ihr Watchdog auslöst, bevor der Server schließt. Behandeln Sie einen Keepalive-Close als Reconnect, nicht als Fehler.

pongPermalink for this section

Antwort auf einen Client-ping.

{ "type": "pong", "timestamp": "2026-02-08T18:47:42.000Z" }

errorPermalink for this section

Fehlerbenachrichtigung. Die Verbindung kann offen bleiben (bei nicht schwerwiegenden Fehlern) oder geschlossen werden (bei Auth-/Limit-Fehlern).

{ "type": "error", "code": "unknown_message_type", "message": "Unknown message type: foobar" }

Die WebSocket-Schicht gibt einen kleinen, festen Satz von Frame-Level-Fehlercodes für Client-Protokollfehler aus. Diese unterscheiden sich von den HTTP-Fehlercodes, die von REST-Endpoints zurückgegeben werden.

CodeBedeutung
invalid_messageFrame konnte nicht als JSON geparst werden oder entsprach nicht der erwarteten Form
unknown_message_typeDas type-Feld ist keiner der Werte auth, subscribe, filter, refresh_token, ping
missing_tokenauth- oder refresh_token-Frame enthielt kein token-Feld
missing_channelssubscribe-Frame enthielt kein nicht-leeres channels-Array
not_authenticatedsubscribe, filter oder refresh_token gesendet, bevor auth erfolgreich war
already_authenticatedClient hat einen zweiten auth-Frame gesendet, nachdem der erste erfolgreich war

WebSocket-Frames können auch die HTTP-ähnlichen Codes invalid_api_key, tier_restricted und too_many_streams enthalten — diese führen dazu, dass der Server die Verbindung nach dem Senden des Frames schließt. Die vollständige Liste finden Sie unter API-Übersicht → Fehlercodes.

SchließcodesPermalink for this section

CodeBedeutungLösung
1000Normales SchließenVom Client oder Server initiiertes sauberes Schließen
1006Abnormales Schließen (clientseitig)Netzwerkabbruch oder Prozesstermination — immer neu verbinden
1009Nachricht zu groß (clientseitig)Das Limit Ihrer Bibliothek für eingehende Nachrichten liegt unter der Snapshot-Frame-Größe von 256KB — erhöhen Sie es (z. B. Python websockets max_size) auf mindestens 512KB
4001Authentifizierungsfehler oder Verdrängung durch eine neuere SessionAPI key prüfen. Lautet der Close-Reason displaced by newer session, hat eine andere Verbindung den einzigen Slot pro Key übernommen — nicht automatisch neu verbinden
4003Berechtigungen haben sich während des Streams geändert (Downgrade, widerrufener Key, entferntes Add-on)Neu verbinden, um mit den aktuellen Berechtigungen zu autorisieren. Hat sich der Tarif tatsächlich geändert, zuerst das beheben — ein Reconnect mit widerrufenem Key oder entferntem Add-on wird bereits bei der Authentifizierung abgelehnt

Code 1006 ist gemäß RFC 6455 reserviert und wird niemals über die Leitung übertragen. Ihre WebSocket-Bibliothek erzeugt ihn lokal, wenn die TCP-Verbindung ohne ordnungsgemäßen Schließ-Handshake unterbrochen wird (Netzwerkausfall, Prozesstermination, OS-Timeout). Der Server hat ihn nicht gesendet. Bei 1006 immer neu verbinden.

SequenznummernPermalink for this section

global_seq ist ein JavaScript-sicherer Dezimal-String-Checkpoint für den Best-Effort-Reconnect-Replay. Wiedergebbare Daten-Frames können auch seq enthalten, denselben prozessglobalen Checkpoint in einer älteren Integer-Darstellung. Diese Werte sind keine verbindungsspezifischen Zähler und sind für einen gefilterten Subscriber nicht fortlaufend: Frames für andere Channels, Bücher und Subscriber verbrauchen ebenfalls Werte.

Beobachtete numerische Lücken zeigen daher keinen Verlust oder eine verworfene Nachricht an. Reagieren Sie auf explizite Wiederherstellungssignale — resync_required, gap_detected und ein abgelehntes Resume — anstatt N+1-Kontinuität vorauszusetzen. Snapshot- und Kontroll-Frames enthalten nicht einheitlich eines der Sequenzfelder. Wiedergegebene Daten behalten ihren ursprünglichen Checkpoint und ergänzen "replay": true.

Die Zustellung über Bücher hinweg kann prozessglobale Werte auch umordnen, sodass ein Client 101 vor 100 beobachten kann. Berechnen Sie niemals einen Reconnect-Checkpoint aus dem zuletzt beobachteten Wert, einem laufenden Maximum, quellenspezifischen Maxima oder scheinbarer Kontinuität. Es gibt keinen clientseitig berechenbaren Checkpoint, der zwischen serverausgestellten Quittungen vorschreitet.

Die sichere fortschreitende Quittung ist last_seq bei einem empfangenen snapshot:complete mit mode: "resume". Bei einer frischen Verbindung oder einem Full-Resync-Fallback ist connected.global_seq ein konservativer Floor, aber committen Sie diesen Floor nicht, bis das folgende frische/vollständige snapshot:complete bestätigt, dass die autoritative Baseline vollständig empfangen wurde. heartbeat.global_seq ist als Quittung unsicher: Es kann einen Wert widerspiegeln, der vor der Zustellung geprägt wurde. Bewahren Sie sichere Checkpoints als Dezimal-Strings ohne JavaScript-Zahlenkonvertierung.

Wiederverbindung mit ReplayPermalink for this section

Verbinden Sie sich bei einer kurzen Unterbrechung mit denselben Channels und Filtern sowie der letzten bestätigten Serverquittung neu. from_seq allein fordert einen Best-Effort-prozesslokalen Replay an:

wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&sport=basketball&sportsbook=draftkings,fanduel&league=nba&from_seq=12900
ParameterWirkung
from_seq=NVersucht Replay streng nach Checkpoint N

Wenn akzeptiert, enthält connected "resumed": true; wiedergegebene Frames sind mit "replay": true markiert, und die Replay-Phase endet mit snapshot:complete in "mode": "resume". Der Empfang von last_seq dieses Abschluss-Frames lässt den sicheren Checkpoint vorschreiten. connected.global_seq bei einem akzeptierten Resume beschreibt das beabsichtigte Replay-Ende, nicht den Beweis, dass alle Replay-Frames den Client erreicht haben. Ein normaler Replay sendet berechtigte Frames streng nach from_seq in Pufferreihenfolge und holt Frames auf, die während dieses Durchlaufs ankamen. Eine breite Odds-only-Lücke kann stattdessen auf den aktuellen geänderten Zeilenstand konsolidiert werden, anstatt jeden Zwischenübergang zu senden. Wenn eine Konsolidierung nicht möglich ist oder ihr Sendelimit überschreitet, fällt der Server explizit auf einen vollständigen Snapshot zurück; er kürzt den Replay nicht stillschweigend. Wenden Sie Updates und Entfernungen idempotent an.

Wenn der Checkpoint nicht eingehalten werden kann, ist das Ergebnis explizit: connected enthält "resumed": false und "fallback_reason", gefolgt von autoritativen Snapshots und snapshot:complete mit "mode": "full_resync". Aktuelle Gründe sind parse_error, foreign_seq, process_restarted, seq_too_old, gap_too_large und disabled.

Wenn ein Replay startet, aber später mit "gap_detected": true abgeschlossen wird, verwerfen Sie den gespeicherten Checkpoint und gleichen Sie über REST ab oder fordern Sie einen frischen Snapshot an, indem Sie sich ohne from_seq neu verbinden. Die neue Verbindung sendet einen neuen autoritativen Snapshot.

from_seq ist eine Latenzoptimierung über den kurzen Replay-Puffer, kein Vollständigkeitsmechanismus. Es gibt keinen fortschreitenden clientseitig berechenbaren Checkpoint zwischen Serverquittungen. Verwenden Sie einen vollständigen Snapshot oder REST-Abgleich, wenn vollständiger Status wichtig ist.

let resumeCheckpoint; let pendingSnapshotFloor; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'connected') { // Ein frischer/Full-Resync-Floor wird erst nach Abschluss seines Snapshots sicher. pendingSnapshotFloor = msg.resumed === true ? undefined : msg.global_seq; } if (msg.type === 'snapshot:complete' && msg.mode === 'resume') { if (msg.gap_detected) { // Der Server hat diesen Durchlauf als lückenhaft gemeldet: der Checkpoint ist nicht wiederverwendbar. resumeCheckpoint = undefined; reconcileThroughRestOrRequestFreshSnapshot(); } else { // Serverquittung: alle Replay-Frames vor dieser Grenze wurden zugestellt. resumeCheckpoint = msg.last_seq; } pendingSnapshotFloor = undefined; } else if ( msg.type === 'snapshot:complete' && (msg.mode === 'full_resync' || msg.mode === undefined) && pendingSnapshotFloor ) { // Die autoritative frische/vollständige Baseline ist jetzt abgeschlossen. resumeCheckpoint = pendingSnapshotFloor; pendingSnapshotFloor = undefined; } if (msg.replay) { console.log('Replayed event:', msg.type); } }; // On reconnect: function reconnect() { const params = new URLSearchParams({ api_key: 'YOUR_KEY', channels: 'ev,odds' }); if (resumeCheckpoint) params.set('from_seq', resumeCheckpoint); ws = new WebSocket(`wss://ws.sharpapi.io?${params}`); }

Die Replay-Aufbewahrung ist nominal und Best-Effort, keine garantierte Dauer. Prozess-Routing, Deployments, zeitbasierter Ablauf, Eintrags-/Byte-Verdrängung und Replay-Limits können das nutzbare Fenster verkürzen. Dauerhaftes resume_id wird derzeit nicht als Kundencheckpoint unterstützt; Live-Produktionsframes lassen es weg. Speichern Sie nur die oben beschriebenen serverausgestellten Checkpoint-Quittungen.

Stand 2026-08-27 läuft die Produktion das dauerhafte Log im Nur-Messung-shadow-Modus. Diese datierte Deployment-Einstellung ist kein Versprechen, dass dauerhaftes Resume verfügbar ist.

Vollständige ResynchronisierungPermalink for this section

Verwenden Sie einen der drei unterstützten Wege zu einem frischen Snapshot:

  1. Verbinden Sie sich ohne from_seq neu und stellen Sie die gewünschten Channels und Filter in der Verbindungs-URL oder in Subscribe-Nachrichten wieder her. Die neue Verbindung sendet unmittelbar nach der Authentifizierung einen vollständigen Snapshot.
  2. Kündigen Sie das Abonnement für den betroffenen Channel und abonnieren Sie ihn neu. Der Übergang macht den Channel neu und sendet einen frischen Snapshot.
  3. Senden Sie resync mit den betroffenen Channels über den offenen Socket — das Abonnement wird niemals getrennt, sodass nichts in einem Abbestellfenster verloren geht. Siehe resync.

Ein doppeltes subscribe oder ein nur-Filter-Update löst keinen Snapshot aus.

Der Server sendet resync_required, wenn Backpressure Live-Deltas verwirft:

{ "type": "resync_required", "reason": "backpressure", "dropped": 54, "message": "Deltas were dropped due to slow consumption. Request /api/v1/odds for a full snapshot or reconnect." }

dropped gibt die Anzahl der Frames an, die für Ihre Verbindung seit der vorherigen resync_required verworfen wurden — nicht ein Lebensdauer-Gesamt und kein flottenweit gültiger Wert. Er ermöglicht es Ihnen, die Lücke zu bemessen: einige Frames auf einem ruhigen Markt erfordern eine andere Entscheidung als mehrere Hundert mitten im Slate.

Der Frame wird nur gesendet, wenn diese Anzahl größer als null ist, sodass der Empfang immer einen echten Verlust bedeutet. Erholen Sie sich durch REST-Abgleich oder durch Verwendung eines der oben genannten Snapshot-Wege; senden Sie resync_required nicht an den Server zurück.

CodebeispielePermalink for this section

// Subscribe to EV opportunities + odds only (skip middles, low_hold, arbitrage) const ws = new WebSocket( 'wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&sport=basketball&league=nba' ); ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'connected': console.log(msg.message, '| tier:', msg.tier, '| channels:', msg.channels); break; case 'subscribed': console.log('Channels:', msg.channels, '| Filters:', msg.sportsbooks, msg.leagues); break; case 'opportunities_snapshot': if (msg.ev) console.log(`EV snapshot: ${msg.ev.length} opportunities`); break; case 'initial': const books = Object.keys(msg.data); console.log(`Odds snapshot: ${books.length} books`); break; case 'snapshot:complete': console.log('All initial data received'); break; case 'odds:update': console.log(`${msg.source}: ${msg.data.length} odds updated`); break; case 'ev:detected': msg.data.forEach(ev => console.log(`+EV: ${ev.selection} at ${ev.ev_percentage}%`) ); break; case 'heartbeat': break; // silent keepalive } }; ws.onclose = (event) => { console.log(`Closed: ${event.code} ${event.reason}`); }; // Send ping every 25s to keep alive setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 25000); // Update channels and filters without reconnecting function updateSubscription(channels, { sports, sportsbooks, leagues } = {}) { ws.send(JSON.stringify({ type: 'subscribe', channels, filters: { sports, sportsbooks, leagues } })); }

Limits für gleichzeitige StreamsPermalink for this section

Das Limit gilt pro API-Key und wird von WebSocket und SSE gemeinsam genutzt. Es gilt nicht pro Verbindungs-URL: Ein zweiter Socket mit anderen Channels erhält keinen eigenen Slot.

PlanMax. gleichzeitige Streams pro Key
Jeder kostenpflichtige Tarif (Streaming über das WebSocket-Add-on, 99 $/Monat)1
Jeder kostenpflichtige Tarif mit bereitgestellten zusätzlichen gleichzeitigen Streams (ein maxStreams-Override pro Key)Die auf dem Key bereitgestellte Anzahl

Zusätzliche gleichzeitige StreamsPermalink for this section

Mehr als einen gleichzeitigen Socket auf einem einzigen Key zu betreiben — ein Hot/Warm-Failover-Paar oder eine Verbindung pro Worker in Ihrer eigenen Flotte — ist eine bereitgestellte Kapazitätserhöhung auf Ihrem Key (ein maxStreams-Override pro Key), keine Code-Änderung auf Ihrer Seite. Es ist ein kostenpflichtiges Add-on in jedem kostenpflichtigen Tarif — kontaktieren Sie den Vertrieb mit der gewünschten Anzahl an Streams; es wirkt ab Ihrer nächsten Verbindung, ohne Key-Rotation und ohne Redeploy.

Prüfen Sie, was Ihnen gewährt wurde, indem Sie streams.max im connected-Ack lesen — es meldet das tatsächlich durchgesetzte Limit, sodass Sie Ihre Grenze nie aus einer Verdrängung ableiten müssen:

"streams": { "max": 4, "active": 2 }

streams.active zählt die Streams, die auf der Sie bedienenden Instanz gehalten werden, während das Limit flottenweit durchgesetzt wird. Es ist also eine Untergrenze: active < max garantiert nicht, dass eine neue Verbindung keine Ihrer eigenen Sessions verdrängt. Behandeln Sie es als Diagnose („Bin ich dabei, meine andere Session zu kicken?”) und behandeln Sie 4001 unabhängig davon.

Einen separaten Key pro Prozess zu erstellen ist die Alternative und erfordert keine Bereitstellung — jeder Key trägt seinen eigenen Slot.

Eine zweite Verbindung mit demselben Key wird nicht abgelehnt — sie verdrängt die erste. Der neue Socket verbindet sich immer, die ältere Verbindung wird mit 4001 displaced by newer session geschlossen („newer wins”). Das entsprechende Signal bei SSE ist ein abschließendes displaced-Event mit reconnect: false.

Ein verdrängter Client sollte sich nicht automatisch neu verbinden. Der Slot gehört jetzt der neueren Session; ein Reconnect würde diese sofort wieder verdrängen und eine Reconnect-Schleife auslösen.

Das Limit wird instanzübergreifend koordiniert — die Slot-Zuordnung liegt in gemeinsam genutztem Zustand, nicht pro Prozess — ein Verteilen der Verbindungen auf mehrere eigene Hosts ist daher kein unterstützter Weg daran vorbei. Für echte parallele Sockets nutzen Sie Zusätzliche gleichzeitige Streams oben. Siehe Eine Verbindung, viele Themen, um viele Sportarten, Ligen und Buchmacher über einen einzigen Socket abzudecken.

429 too_many_streams wird beim HTTP-Upgrade nur zurückgegeben, wenn ein Key, der Streaming-Zugang bereits HAT, auf null Slots kommt — ein expliziter maxStreams: 0-Override. Ein Key ohne Streaming-Zugang wird bereits vorher mit 403 tier_restricted abgelehnt, vor dem Upgrade, und erreicht den Limiter nie. In einem normalen kostenpflichtigen Tarif erfolgt stattdessen Verdrängung.

Best PracticesPermalink for this section

  1. Channels verwenden — Abonnieren Sie nur die Daten, die Sie benötigen. channels=low_hold überspringt den gesamten Quoten-Dump und andere Opportunity-Typen und reduziert die initiale Payload von Megabyte auf Kilobyte
  2. Pings alle 25 Sekunden senden — Der Server sendet alle 30s Heartbeats, aber explizite Pings verhindern Proxy-/Firewall-Timeouts
  3. Filter verwenden — Übergeben Sie die Parameter sport, sportsbook, league, market und event_id, um Daten innerhalb Ihrer abonnierten Channels einzugrenzen
  4. Schwellenwerte setzen — Verwenden Sie min_ev und min_profit, um Opportunities mit geringem Wert serverseitig herauszufiltern und das Rauschen zu reduzieren
  5. Aktualisierung über subscribe — Ändern Sie Channels, Filter und Schwellenwerte ohne Wiederverbindung
  6. Schließcodes behandeln — 4001 bedeutet ungültiger Schlüssel oder Verdrängung durch eine neuere Session (am Close-Reason unterscheidbar), 4003 bedeutet, dass sich die Berechtigungen während des Streams geändert haben (Downgrade, widerrufener Key, entferntes Add-on) — neu verbinden, um zu autorisieren, nachdem Sie geprüft haben, dass der Tarif es noch zulässt
  7. Serverquittungen verfolgen — Committen Sie snapshot:complete.last_seq nach einem erfolgreichen Resume oder den ausstehenden connected.global_seq-Floor erst nach Abschluss des frischen/vollständigen Snapshots; leiten Sie from_seq niemals aus beliebigen Daten oder Heartbeat-Frames ab
  8. Wiederverbindung implementieren — Im Gegensatz zu SSE führt WebSocket keine automatische Wiederverbindung durch. Verwenden Sie exponentielles Backoff (1s, 2s, 4s, …) mit from_seq-Replay für kurze Ausfälle
  9. Auf snapshot:complete warten — Dies signalisiert, dass alle initialen Daten gesendet wurden. Blenden Sie Ladezustände nach Erhalt aus
  10. odds:removed behandeln — Entfernen Sie Quoten aus Ihrem lokalen Status, wenn Sie diese Nachricht erhalten, um die Anzeige veralteter Daten zu vermeiden
  11. Ungenutzte Verbindungen schließen — Jeder Schlüssel erlaubt standardmäßig 1 gleichzeitigen Stream; eine zweite Verbindung mit demselben Schlüssel verdrängt die ältere (Schließcode 4001)

VerwandtPermalink for this section

Last updated on