Skip to Content
API-ReferenzSSE-Stream

Unified Stream

GET /api/v1/stream — Echtzeit-Updates für Quoten und Opportunities über Server-Sent Events (SSE).

Erfordert das WebSocket-Add-on ($99/Monat) auf einem beliebigen kostenpflichtigen Tarif oder Enterprise (enthalten). Der kostenlose Tarif unterstützt kein Streaming.

AuthentifizierungPermalink for this section

Übergeben Sie Ihren API key über den Header oder als Query-Parameter:

# Header (empfohlen für serverseitige Nutzung) curl -H "X-API-Key: sk_live_your_key" \ https://api.sharpapi.io/api/v1/stream # Query-Parameter (erforderlich für Browser-EventSource) https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key

Query-ParameterPermalink for this section

ParameterTypStandardBeschreibung
channelstringopportunitiesWas gestreamt werden soll: odds, opportunities, gamestate (nur Enterprise) oder all. Akzeptiert auch den Plural-Alias channels. Eine kommagetrennte Liste mit mehr als einem Kanal wird auf all reduziert — siehe Hinweis unten.
sportstringalleFilter nach Sportart(en), kommagetrennt (z. B. basketball, football, ice_hockey)
sportsbookstringtariferlaubtFilter nach Sportsbook(s), kommagetrennt
leaguestringalleFilter nach Liga(en), kommagetrennt
event_idstringalleFilter nach Event-ID(s), kommagetrennt
marketstringalleFilter nach Markttyp(en), kommagetrennt (z. B. moneyline, point_spread, total_points, player_points)
min_evnumber2.0Minimaler EV-Prozentsatz für +EV-Opportunity-Events
min_profitnumber0.5Minimaler Gewinnprozentsatz nur für Arbitrage-Events (gilt nicht für die Low-Hold-Filterung)
statestring—US-Bundesstaatencode nur für das Routing von Deeplinks; filtert oder verändert keine Quoten. Akzeptiert die 50 US-Bundesstaatencodes sowie dc. Ein gültiger Code ergänzt Quoten-Deeplinks um ?state=. Fehlende, leere oder nicht unterstützte Codes erzeugen keinen Zusatz; die Weiterleitung verwendet dann ihren eigenen Standard pa. Nicht unterstützte Codes erzeugen nach connected ein filter_warning; der Handshake ist erfolgreich.
api_keystring—API key (Alternative zur Header-Authentifizierung für Browser-EventSource)

Channel-OptionenPermalink for this section

ChannelÜbermittelte EventsAnwendungsfall
oddssnapshot, odds:update, odds:removed, heartbeatQuotenbewegungen verfolgen
opportunitiessnapshot, ev:detected/expired, arb:detected/expired, middles:detected/expired, low_hold:detected/expired, heartbeatBei Opportunities benachrichtigen
gamestategamestate:snapshot, gamestate:update, gamestate:final, heartbeatLive-Spielstände, Perioden, Uhren und situative Daten pro Event. Jedes gamestate:update sendet die komplette aktuelle Slate erneut — auf SSE gibt es kein gamestate:removed (siehe gamestate:update). Nur Enterprise-Tarif. Siehe Live Game State für den vollständigen Feldkatalog.
allAlle Event-TypenVollständiges Echtzeitbild

Ein Abonnement pro SSE-Stream. Aus Gründen der Parität mit der WebSocket-API akzeptiert der Endpoint sowohl channel als auch den Plural channels, und beide tolerieren einen kommagetrennten Wert. Eine SSE-Verbindung trägt jedoch ein einziges Abonnement: Wenn du mehr als einen gültigen Kanal übergibst (z. B. ?channels=odds,opportunities), wird die Anfrage auf channel=all reduziert, statt einen Fehler zurückzugeben. Ein einzelner Wert (?channel=odds) streamt nur diesen Kanal. Um gezielt eine bestimmte Teilmenge von Kanälen zu abonnieren, verwende die WebSocket-API, die echtes Multi-Channel-Filtern via channels= über eine einzige Verbindung unterstützt.

Komfort-RoutenPermalink for this section

RouteEntspricht
GET /api/v1/stream/odds/api/v1/stream?channel=odds
GET /api/v1/stream/opportunities/api/v1/stream?channel=opportunities
GET /api/v1/stream/gamestate/api/v1/stream?channel=gamestate
GET /api/v1/stream/all/api/v1/stream?channel=all
GET /api/v1/stream/events/:eventId/api/v1/stream?channel=odds&event_id=:eventId

SSE-Event-TypenPermalink for this section

connectedPermalink for this section

Wird unmittelbar nach dem Aufbau des Streams gesendet.

event: connected data: {"stream_id":"stream_1704960637000","channel":"all","filters":{"sportsbook":null,"sport":["basketball"],"league":["nba"],"event":null,"market":null},"reconnected":false}
FeldTypBeschreibung
stream_idstringEindeutige Stream-Kennung
channelstringEcho des angeforderten Channels (odds, opportunities oder all)
filtersobjectEcho der aktiven Filter
reconnectedbooleantrue, wenn es sich um eine Wiederverbindung über Last-Event-ID handelt — auch bei einem erfolgreichen Resume, daher kein Signal zum Leeren des Status
resumedbooleanVorhanden, wenn Sie sich mit einer Last-Event-ID erneut verbunden haben. true → der Server sendet die verpassten odds-Events erneut (mit replayed_count), statt einen Snapshot zu schicken; kann er die Wiedergabe nicht abschließen, folgt doch noch ein vollständiger Snapshot, gekennzeichnet mit mode: "full_resync" auf snapshot:complete. false → der Server konnte nicht fortsetzen; ein vollständiger Snapshot folgt und fallback_reason nennt den Grund
replayed_countnumberBei resumed: true — wie viele gepufferte Events erneut gesendet werden
fallback_reasonstringBei resumed: false — warum das Resume zurückfiel (z. B. seq_too_old, process_restarted, filter_changed, channel_unsupported). Rein informativ; die Behandlung ist in jedem Fall dieselbe: den vollständigen Snapshot annehmen. Vollständige Werteliste
trialobject | undefinedVorhanden, wenn der Benutzer sich in einer Streaming-Testphase befindet. Enthält active, expires_at, remaining_hours, max_streams

snapshotPermalink for this section

Vollständiger Datenabzug, der nach connected gesendet wird. Enthält alle aktuellen Quoten oder Opportunities, die zu Ihren Filtern passen. Große Datensätze werden in mehrere snapshot-Events aufgeteilt (jeweils bis zu 1000 Einträge).

Jedes Quotenobjekt im Snapshot enthält alle Felder — dies ist die vollständige Odds-Struktur, die Ihr Client lokal speichern sollte. Nachfolgende odds:update-Events senden nur geänderte Felder (siehe unten).

Auf den Channels opportunities und all tragen Opportunity-Chunks ihr Array unter dem Opportunity-Typ statt unter odds — ev, arbitrage, middles oder low_hold — mit denselben Feldern count, total, offset und has_more. Speichern Sie diese Einträge ebenfalls nach id: Ein späteres ev:detected (oder arb:/middles:/low_hold:detected) mit derselben id ist ein Update eines dieser Einträge.

event: snapshot id: evt_00001 data: {"odds":[{"id":"123456","sportsbook":"draftkings","event_id":"nba_phosuns_phi76ers_2026-02-08","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","market_type":"moneyline","selection":"PHO Suns","selection_type":"away","odds_american":-155,"odds_decimal":1.645,"odds_probability":0.608,"line":null,"event_start_time":"2026-02-08T19:00:00Z","is_live":false,"timestamp":"2026-02-08T18:47:20Z","deep_link":"https://sportsbook.draftkings.com/event/..."}],"count":1000,"total":3200,"offset":0,"has_more":true}
FeldTypBeschreibung
oddsarrayArray vollständiger Odds-Objekte (siehe Odds-Endpoint für alle Felder)
countnumberAnzahl der Quoten in diesem Chunk
totalnumberGesamtzahl der Quoten, die zu den Filtern passen
offsetnumberOffset dieses Chunks im Gesamtergebnis
has_morebooleantrue, wenn weitere snapshot-Chunks folgen

snapshot:completePermalink for this section

Signalisiert, dass alle initialen Snapshots gesendet wurden. Nach Erhalt können Ladeanzeigen sicher ausgeblendet werden.

event: snapshot:complete id: evt_00005 data: {"status":"ready","books":["draftkings","fanduel"],"total_odds":3200}

odds:updatePermalink for this section

Wird ausgelöst, wenn sich die Quoten für ein Sportsbook ändern. Wird nur auf den Channels odds oder all gesendet.

Kompakte Delta-Payload. Delta-Events enthalten nur Felder, die sich zwischen Updates ändern können — id, odds_american, odds_decimal, odds_probability, line, is_live und timestamp. Statische Felder wie sportsbook, sport, league, home_team, away_team, market_type, selection, deep_link und event_start_time sind in Deltas nicht enthalten. Führen Sie jedes Delta in Ihre lokale Quoten-Map über die id zusammen, indem Sie die im initialen snapshot empfangenen vollständigen Objekte verwenden. Manche Zeilen kommen vollständig statt als Delta: Eine Zeile mit einer id, die Sie noch nicht halten, wird komplett, mit allen Odds-Feldern, im selben odds:update-Event gesendet — speichern Sie sie so, wie sie ist, statt sie zu überspringen. Siehe Migration: Kompakte SSE-Deltas unten.

event: odds:update id: evt_00042 data: {"odds":[{"id":"123456","odds_american":-150,"odds_decimal":1.667,"odds_probability":0.6,"line":null,"is_live":false,"timestamp":"2026-02-08T18:47:38Z"}],"count":1,"book":"draftkings","partial":false}

Felder des Delta-Objekts (OddsDelta):

FeldTypBeschreibung
idstringEindeutige Quoten-ID — entspricht der id aus dem initialen Snapshot
odds_americannumberAktualisierte amerikanische Quote (z. B. -150)
odds_decimalnumberAktualisierte dezimale Quote (z. B. 1.667)
odds_probabilitynumberAktualisierte implizite Wahrscheinlichkeit (z. B. 0.6)
linenumber | nullAktualisierte Line/Spread (z. B. -3.5) oder null für Moneyline
is_livebooleanOb das Event derzeit live ist
timestampstringISO-8601-Zeitpunkt, zu dem SharpAPI diese Quote zuletzt durch seine Pipeline aktualisiert hat — wird in jedem Ingest-Zyklus weitergeschaltet. Ein Feed-Aktualitäts- / Liveness-Signal; es ist NICHT der Zeitpunkt, zu dem sich der Preis zuletzt geändert hat. Siehe timestamp verstehen.

Envelope-Felder:

FeldTypBeschreibung
oddsarrayArray von OddsDelta-Objekten (kompakt — nur dynamische Felder)
countnumberAnzahl der Quoten in diesem Chunk
bookstringSportsbook, das sich geändert hat (z. B. "draftkings")
partialbooleantrue, wenn weitere Chunks für diesen Update-Batch folgen

ev:detectedPermalink for this section

Eine neue Opportunity mit positivem Erwartungswert oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Wird nur auf den Channels opportunities oder all gesendet.

:detected bedeutet neu oder aktualisiert — Upsert per id. Seit dem 2026-09-26 sendet der SSE-Stream 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 im gewöhnlichen ev:detected-, arb:detected-, middles:detected- oder low_hold:detected-Event, so wie es die WebSocket-API tut. Vor diesem Datum lieferte der SSE-Stream diese Updates uneinheitlich — manche Inhaltsänderungen erreichten den Client unter derselben id, die meisten nicht —, sodass ein Client, der eine wiederholte id übersprang, eine veraltete Version behalten konnte, bis die Opportunity ablief oder er sich neu verband.

  • Speichern Sie Opportunities nach id und ersetzen Sie die gespeicherte Version durch jeden :detected-Eintrag.
  • Lösen Sie Alerts nur für eine id aus, die Sie noch nicht gesehen haben. Eine bekannte id ist ein Update, keine neue Opportunity. Zählen Sie die ids aus den snapshot-Chunks als gesehen, und vergessen Sie eine id, sobald *:expired sie auflistet.

*:expired-Events und das Payload-Format bleiben unverändert.

event: ev:detected data: {"opportunities":[{"id":"a1b2c3d4e5f6","game_id":"nba_phosuns_phi76ers_2026-02-08","ev_percentage":4.35,"odds_american":-105,"odds_decimal":1.952,"no_vig_odds":-101,"selection":"PHO Suns -3.5","market":"point_spread","line":-3.5,"sportsbook":"draftkings","game":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","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"}],"count":1,"type":"ev"}

Alle vier :detected-Events verwenden diesen Envelope:

FeldTypBeschreibung
opportunitiesarrayNeue oder aktualisierte Opportunities. Jeden Eintrag nach id einfügen oder ersetzen (Upsert)
countnumberAnzahl der Einträge in opportunities
typestringev, arbitrage, middles oder low_hold

ev:expiredPermalink for this section

Eine zuvor erkannte +EV-Opportunity ist nicht mehr verfügbar. expired listet die zu entfernenden ids; count und type wie bei ev:detected.

event: ev:expired data: {"expired":["a1b2c3d4e5f6"],"count":1,"type":"ev"}

arb:detectedPermalink for this section

Eine neue Arbitrage-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Wird nur auf den Channels opportunities oder all gesendet.

event: arb:detected data: {"opportunities":[{"id":"61c501b83ce932d1","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"profit_percent":2.8,"implied_total":97.2,"is_live":false,"legs":[{"sportsbook":"draftkings","selection":"PHO Suns","odds_american":150,"odds_decimal":2.5,"implied_probability":0.4,"stake_percent":41.4},{"sportsbook":"fanduel","selection":"PHI 76ers","odds_american":-130,"odds_decimal":1.769,"implied_probability":0.5652,"stake_percent":58.6}],"detected_at":"2026-02-08T18:47:21.000Z"}],"count":1,"type":"arbitrage"}

arb:expiredPermalink for this section

Eine zuvor erkannte Arbitrage-Opportunity ist nicht mehr verfügbar.

event: arb:expired data: {"expired":["61c501b83ce932d1"],"count":1,"type":"arbitrage"}

middles:detectedPermalink for this section

Eine neue Middle-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Wird nur auf den Channels opportunities oder all gesendet.

event: middles:detected data: {"opportunities":[{"id":"middle_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","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":2.1,"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.5,"deep_link":null},"middle_size":1,"middle_numbers":[23],"middle_probability":0.12,"expected_value":3.5,"quality_score":85,"detected_at":"2026-02-08T18:47:22.000Z"}],"count":1,"type":"middles"}

middles:expiredPermalink for this section

Eine zuvor erkannte Middle-Opportunity ist nicht mehr verfügbar.

event: middles:expired data: {"expired":["middle_abc123"],"count":1,"type":"middles"}

low_hold:detectedPermalink for this section

Eine neue Low-Hold-Opportunity oder eine aktualisierte Version einer bereits gesendeten (gleiche id). Wird nur auf den Channels opportunities oder all gesendet.

event: low_hold:detected data: {"opportunities":[{"id":"lowhold_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"home_team":"PHI 76ers","away_team":"PHO Suns","start_time":"2026-02-08T19:00:00.000Z","hold_percentage":1.2,"is_live":false,"all_books":["draftkings","fanduel"],"side1":{"selection":"PHO Suns","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":"PHI 76ers","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"}],"count":1,"type":"low_hold"}

low_hold:expiredPermalink for this section

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

event: low_hold:expired data: {"expired":["lowhold_abc123"],"count":1,"type":"low_hold"}

gamestate:snapshotPermalink for this section

Vollständige aktuelle Live-Slate, einmal nach connected auf dem Channel gamestate (oder all) gesendet. Die Payload ist eine flache Liste von Event-Zeilen — jede Zeile hat dieselbe Form wie ein Live Game State-REST-Event, plus die event_id.

event: gamestate:snapshot data: {"data":[{"event_id":"atp_jdoe_rroe_2026-07-03","home_team":"J. Doe","away_team":"R. Roe","sport":"tennis","league":"atp","home_score":9,"away_score":10,"game_period":"S3","is_live":true,"primary_book":"draftkings","book_count":4}]}

gamestate:updatePermalink for this section

Wird bei jedem Gamestate-Update-Zyklus ausgelöst. Wird nur auf den Channels gamestate oder all gesendet.

Vollständige Slate-Neuemission — kein Delta. Anders als bei odds:update trägt jedes SSE-gamestate:update die komplette aktuelle Live-Slate, die zu Ihren Filtern passt. Es gibt kein gamestate:removed-Event auf SSE — ein beendetes Event bleibt für das Carry-Fenster als status: "final"-Zeile in der Payload und erscheint danach nicht mehr. Ersetzen Sie Ihren lokalen Gamestate bei jedem gamestate:update vollständig; führen Sie ihn nicht als Delta zusammen, sonst bleiben beendete Events für immer in Ihrem Zustand. Ein Sonderfall: Mit gesetzten sport/league-Filtern sendet ein Zyklus, der auf null Events passt, überhaupt kein gamestate:update (nur ungefilterte Streams erhalten die leere {"data": []}-Payload), sodass das letzte beendete Event nie ersetzt wird — wenn Heartbeats weiterlaufen, Updates aber ausbleiben, behandeln Sie die Slate als möglicherweise leer und lassen Sie nicht aktualisierte Zeilen verfallen. Wenn Sie inkrementelle Zustellung benötigen — Updates geänderter Zeilen plus explizite gamestate:removed-ID-Listen — verwenden Sie stattdessen die WebSocket-API; die Feldformen stehen in der Live-Game-State-Referenz.

event: gamestate:update data: {"data":[{"event_id":"atp_jdoe_rroe_2026-07-03","home_team":"J. Doe","away_team":"R. Roe","sport":"tennis","league":"atp","home_score":10,"away_score":10,"game_period":"S3","is_live":true,"primary_book":"draftkings","book_count":4}]}

Der Gamestate-Channel ist über SSE außerdem nicht fortsetzbar: Das Last-Event-ID-Replay deckt nur den odds-Channel ab. Ein Gamestate-Reconnect bootstrappt immer neu mit einem frischen gamestate:snapshot — behandeln Sie jeden Reconnect als Neustart: lokalen Gamestate löschen und aus dem neuen Snapshot neu aufbauen. Beachten Sie, dass Gamestate-Frames (und Heartbeats) keine SSE-id:-Zeile tragen; eine einfache EventSource auf dem gamestate-Channel verbindet sich daher ohne Last-Event-ID neu — ihr connected-Event trägt weder resumed noch fallback_reason. Diese Schlüssel erscheinen nur, wenn der Reconnect tatsächlich eine Last-Event-ID mitsendet (z. B. auf dem all-Channel, wo Odds-Events den Resume-Cursor setzen, oder bei einem manuell gesetzten Header): Der Server bestätigt dann mit resumed: false und fallback_reason: "channel_unsupported".

gamestate:finalPermalink for this section

Wird einmal gesendet, wenn ein Event endet, und trägt dessen terminale Zeile (status: "final", Endstand, completed_at). Gleiche {"data": [...]}-Hülle, gleicher Zugang und gleiche sport/league-Filter wie gamestate:update, keine id:-Zeile. Die Zeile bleibt während des Carry-Fensters auch in jedem gamestate:update, dieser Frame ist also eine Ankündigung, nicht die einzige Zustellung. Er kann sich gelegentlich wiederholen — deduplizieren Sie auf (event_id, completed_at). Siehe Abschlussmeldungen.

event: gamestate:final data: {"data":[{"event_id":"atp_jdoe_rroe_2026-07-03","home_team":"J. Doe","away_team":"R. Roe","sport":"tennis","league":"atp","sets_home":1,"sets_away":2,"score_type":"sets","winner":"away","status":"final","completed_at":"2026-07-03T19:42:10Z","is_live":false,"stale":false}]}

odds:lockedPermalink for this section

Wird ausgelöst, wenn ein Markt ausgesetzt/geschlossen wird (z. B. nach einem Tor, während einer Linienbewegung oder bei einer Sperre in der Schlussphase) — der Preis ist eingefroren, die Auswahl aber nicht mehr wettbar. Trägt die ausgesetzte Teilmenge des aktuellen Deltas, mit derselben Payload-Form wie odds:update und is_active: false. Wird nur auf den Channels odds oder all gesendet.

Es ist ein dediziertes Sperr-Signal und es ist ergänzend — dieselben Zeilen kommen auch in odds:update mit is_active: false an, sodass Clients, die is_active bereits auswerten, odds:locked nicht zusätzlich abonnieren müssen. Verwenden Sie es, wenn Sie ein dediziertes Sperr-Signal wollen, ohne jedes odds:update zu parsen.

event: odds:locked id: 3f9c2a1b:12851 data: {"odds":[{"id":"123456","odds_american":-2500,"is_live":true,"is_active":false,"timestamp":"2026-02-08T18:47:38Z"}],"count":1,"book":"pinnacle","partial":false}

Ein wieder öffnender Markt sendet ein normales odds:update mit is_active: true (und einem frischen Preis). Märkte, die ein Buchmacher vollständig entfernt, kommen stattdessen über odds:removed.

Der Envelope ist derselbe wie bei odds:update, einschließlich des Felds replay: odds:locked-Frames, die während eines Resume erneut gesendet werden, tragen "replay": true, genau wie wiederholte odds:update- und odds:removed-Frames.

odds:removedPermalink for this section

Quoten, die von einem Sportsbook entfernt wurden (z. B. Markt zurückgezogen, Event abgeschlossen). Wird nur auf den Channels odds oder all gesendet.

event: odds:removed id: evt_00051 data: {"ids":["123456","789012"],"count":2,"book":"draftkings"}
FeldTypBeschreibung
idsstring[]Quoten-IDs, die aus dem lokalen Status entfernt werden sollen
countnumberAnzahl der entfernten Quoten
bookstringSportsbook, das die Quoten entfernt hat

heartbeatPermalink for this section

Keep-Alive-Signal, das alle 30 Sekunden gesendet wird. Wenn Sie innerhalb von 60 Sekunden keinen Heartbeat erhalten, ist die Verbindung möglicherweise veraltet.

event: heartbeat data: {"timestamp":"2026-01-26T02:11:07.846Z"}

resync_requiredPermalink for this section

Wird gesendet, wenn Live-Deltas übersprungen wurden, weil Ihr Client zu langsam konsumiert hat (siehe Slow-Consumer-Policy). Wird zugestellt, sobald der Rückstau abgebaut ist, vor dem nächsten Daten-Event — Ihr lokaler Zustand ist jetzt unvollständig. Rufen Sie /api/v1/odds für Ihren Filterbereich erneut ab oder verbinden Sie sich für einen frischen Snapshot neu.

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

displacedPermalink for this section

Wird als letztes Event gesendet, wenn eine neuere Verbindung mit demselben API-Key den Stream-Slot übernimmt (newer wins — siehe Eine Verbindung, viele Themen); der Stream wird danach geschlossen. reconnect: false ist maßgebend: Verbinden Sie sich nicht automatisch neu, sonst verdrängen Sie Ihre eigene neuere Session in einer Schleife.

event: displaced data: {"code":"too_many_streams","message":"Displaced by a newer connection on the same API key (newer-wins). This stream was closed because another session took the single per-key stream slot.","reconnect":false,"hint":"Run one stream per key. To stream from multiple processes concurrently, request a maxStreams increase for your key or mint a separate key per process at https://sharpapi.io/dashboard.","docs":"https://docs.sharpapi.io/streaming/single-connection"}

Der hint ist der wörtliche Text des Servers. Die dort genannte Erhöhung ist ein maxStreams-Override pro Key, verkauft als kostenpflichtiges Add-on in jedem kostenpflichtigen Tarif — siehe Limits für gleichzeitige Streams.

errorPermalink for this section

Behebbarer Fehler im Stream. Die Verbindung bleibt geöffnet.

event: error data: {"code":"upstream_error","message":"Temporary issue fetching DraftKings data. Will retry."}

WiederverbindungPermalink for this section

SSE unterstützt automatische Wiederverbindung über den Last-Event-ID-Header. Auf dem odds-Kanal löst der Server dabei eines von zwei Ergebnissen aus, und er sagt Ihnen immer, welches:

  1. Resume (connected mit resumed: true) — die verpassten odds-Events werden aus einem Best-Effort-Fenster erneut gesendet, jedes mit "replay": true; es kommt kein Snapshot. Behalten Sie Ihren lokalen Status — die erneut gesendeten Deltas bringen ihn auf den aktuellen Stand. snapshot:complete mit mode: "resume" markiert den Übergang zu Live-Daten.
  2. Vollständiger Resync (connected mit resumed: false und einem fallback_reason) — der Server konnte nicht erneut senden. Ein frischer vollständiger Snapshot folgt, und sein snapshot:complete trägt mode: "full_resync".

Ein resumed: true bedeutet, dass die Wiedergabe begonnen hat, nicht dass sie abgeschlossen wurde: Kommen danach snapshot-Chunks, war es doch Pfad 2 — leeren Sie Ihren lokalen Status beim ersten dieser Chunks. Kommt überhaupt kein snapshot-Chunk an, weil nichts mehr zu Ihren Filtern passt, leeren Sie ihn, sobald snapshot:complete mode: "full_resync" meldet. Nur odds-Events tragen id:-Zeilen — gamestate-, opportunities- und all-Wiederverbindungen nehmen immer den Weg über den vollständigen Snapshot.

// Browser handhaben dies automatisch mit EventSource. // Für benutzerdefinierte Clients setzen Sie den Header bei der Wiederverbindung: const headers = { 'X-API-Key': 'YOUR_KEY', 'Last-Event-ID': 'evt_00042' };

Leeren Sie Ihren lokalen Status nur dann, wenn ein vollständiger Snapshot folgt: bei einem connected ohne resumed: true; beim ersten snapshot-Chunk nach einem resumed: true; oder, wenn ein vollständiger Snapshot nach einem resumed: true überhaupt keinen snapshot-Chunk enthält, bei dem snapshot:complete, das mode: "full_resync" meldet. Leeren Sie nicht bei jeder Wiederverbindung und nicht bei reconnected: true — das ist auch bei einem erfolgreichen Resume gesetzt, und dort folgt kein Snapshot, in den Sie die Deltas mischen könnten. Wenn Sie vor einem echten vollständigen Snapshot nicht leeren, vermischen sich veraltete Quoten aus der vorherigen Sitzung mit frischen Daten.

Browser-EventSource verarbeitet Last-Event-ID und Wiederverbindungen automatisch. Für die Wiederverbindung selbst ist kein zusätzlicher Code erforderlich, jedoch müssen Sie das Leeren des Status auf der Clientseite handhaben.

CodebeispielePermalink for this section

// Lokale Quoten-Map — nach Quoten-ID indiziert, speichert vollständige Odds-Objekte aus dem Snapshot. // Delta-Events werden anhand der ID in diese Map zusammengeführt. const oddsMap = new Map(); let resuming = false; // von `connected` mit `resumed: true` gesetzt // Opportunities nach Typ, jeweils nach `id` indiziert. Ein `:detected`-Eintrag ist eine neue // Opportunity ODER eine aktualisierte Version einer bereits gehaltenen (gleiche `id`). const oppsByType = { ev: new Map(), arbitrage: new Map(), middles: new Map(), low_hold: new Map() }; // Upsert nach `id`. Gibt nur beim ersten Auftreten einer `id` true zurück — // darauf alarmieren, nicht auf jeden `:detected`-Eintrag. function upsertOpp(type, opp) { const isNew = !oppsByType[type].has(opp.id); oppsByType[type].set(opp.id, opp); // ersetzt die gehaltene Version return isNew; } const eventSource = new EventSource( 'https://api.sharpapi.io/api/v1/stream?channel=all&league=nba&api_key=YOUR_KEY' ); eventSource.addEventListener('connected', (e) => { const { stream_id, channel, resumed } = JSON.parse(e.data); // Lokale Quoten nur bei einem Resume behalten (`resumed: true`): dann werden die // verpassten Deltas erneut gesendet statt eines Snapshots. Auf jeden anderen // Connect folgt ein vollständiger Snapshot — also jetzt leeren. Nicht // `reconnected` verwenden: das ist auch bei einem Resume `true`. resuming = resumed === true; if (!resuming) oddsMap.clear(); // Opportunities werden nie per Resume fortgesetzt: jeder Connect sendet sie erneut im `snapshot`. for (const map of Object.values(oppsByType)) map.clear(); console.log(`Stream ${stream_id} connected (${channel})`); }); eventSource.addEventListener('snapshot', (e) => { const data = JSON.parse(e.data); // Ein Snapshot nach `resumed: true` heißt: der Server hat das Resume aufgegeben, // dieser Snapshot ersetzt Ihren Status. if (resuming) { oddsMap.clear(); resuming = false; } // Opportunity-Chunks tragen `ev` / `arbitrage` / `middles` / `low_hold` statt `odds` for (const type of Object.keys(oppsByType)) { for (const opp of data[type] ?? []) oppsByType[type].set(opp.id, opp); } if (!data.odds) return; // Vollständige Odds-Objekte nach ID indiziert speichern for (const odd of data.odds) { oddsMap.set(odd.id, odd); } console.log(`Snapshot chunk: ${data.count} odds (${oddsMap.size}/${data.total} total)`); }); eventSource.addEventListener('snapshot:complete', (e) => { const { mode } = JSON.parse(e.data); // "resume", "full_resync" oder bei einem ersten Connect nicht vorhanden // Ein vollständiger Snapshot kann ganz ohne `snapshot`-Chunk kommen — nichts passt mehr // zu Ihren Filtern —, dann lief das Leeren im `snapshot`-Handler nie. Die `resuming`- // Prüfung verhindert, dass hier ein gerade gespeicherter Snapshot gelöscht wird. if (mode === 'full_resync' && resuming) oddsMap.clear(); resuming = false; console.log(`Snapshot complete: ${oddsMap.size} odds loaded`); }); eventSource.addEventListener('odds:update', (e) => { const { odds, book } = JSON.parse(e.data); // Kompakte Deltas in den lokalen Status zusammenführen — nur dynamische Felder werden gesendet for (const delta of odds) { const existing = oddsMap.get(delta.id); if (existing) { Object.assign(existing, delta); // Geänderte Felder zusammenführen } else { // Eine `id`, die Sie noch nicht halten, kommt als vollständige Zeile — so speichern oddsMap.set(delta.id, delta); } } console.log(`${book}: ${odds.length} odds updated`); }); eventSource.addEventListener('odds:removed', (e) => { const { ids, book } = JSON.parse(e.data); for (const id of ids) { oddsMap.delete(id); } console.log(`${book}: ${ids.length} odds removed`); }); // Jeder `:detected`-Eintrag wird per Upsert gespeichert; nur eine erstmals gesehene `id` wird als neu geloggt. eventSource.addEventListener('ev:detected', (e) => { const { opportunities: opps } = JSON.parse(e.data); opps.forEach(opp => { if (upsertOpp('ev', opp)) console.log(`+EV: ${opp.selection} at ${opp.ev_percentage}%`); }); }); eventSource.addEventListener('arb:detected', (e) => { const { opportunities: arbs } = JSON.parse(e.data); arbs.forEach(arb => { if (upsertOpp('arbitrage', arb)) console.log(`Arb: ${arb.profit_percent}% profit`); }); }); eventSource.addEventListener('middles:detected', (e) => { const { opportunities: middles } = JSON.parse(e.data); middles.forEach(m => { if (upsertOpp('middles', m)) console.log(`Middle: ${m.event_name} — EV ${m.expected_value}%`); }); }); eventSource.addEventListener('low_hold:detected', (e) => { const { opportunities: holds } = JSON.parse(e.data); holds.forEach(h => { if (upsertOpp('low_hold', h)) console.log(`Low hold: ${h.hold_percentage}%`); }); }); // `:expired` listet die ids, die weg sind. Vergessen Sie sie, damit eine `id`, // die später zurückkommt, wieder als neu zählt. for (const prefix of ['ev', 'arb', 'middles', 'low_hold']) { eventSource.addEventListener(`${prefix}:expired`, (e) => { const { expired, type } = JSON.parse(e.data); // type: 'ev' | 'arbitrage' | 'middles' | 'low_hold' for (const id of expired) oppsByType[type].delete(id); }); } eventSource.addEventListener('heartbeat', () => { console.log('Connection alive'); }); eventSource.onerror = () => { console.log('Connection lost, auto-reconnecting...'); };

Limits für gleichzeitige StreamsPermalink for this section

Das Limit gilt pro API-Key und wird von SSE und WebSocket gemeinsam genutzt. Es gilt nicht pro Verbindungs-URL: Eine zweite Verbindung mit anderen Filtern erhält keinen eigenen Slot.

TarifMax. 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 — bis zur Freigabe des Overrides weiterhin 1; Vertrieb kontaktieren

Das Öffnen eines zweiten Streams mit demselben Key gibt keinen Fehler zurück — es verdrängt den ersten. Die neue Verbindung ist immer erfolgreich, die ältere wird geschlossen („newer wins”).

Die verdrängte Seite wird ausdrücklich benachrichtigt:

  • SSE — ein abschließendes displaced-Event mit reconnect: false, danach Teardown
  • WebSocket — Close-Code 4001 displaced by newer session

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.

Bei SSE erfordert das eine ausdrückliche Aktion: Eine native EventSource verbindet sich nach jedem serverseitigen Teardown von selbst neu, und reconnect: false im Event-Payload verhindert das nicht — dieses Feld ist ein Hinweis an Ihren Code, keine Browser-Anweisung. Rufen Sie in Ihrem displaced-Handler eventSource.close() auf.

429 too_many_streams wird weiterhin zurückgegeben, aber nur, wenn ein Key, der Streaming-Zugang bereits HAT, auf null Slots kommt — ein expliziter maxStreams: 0-Override. Ein Key ohne Streaming-Zugang erreicht den Limiter gar nicht: Er wird bereits vorher mit 403 tier_restricted abgelehnt. In einem normalen kostenpflichtigen Tarif erfolgt Verdrängung statt eines 429.

Streams verwaltenPermalink for this section

  • Gezählt werden offene Verbindungen pro API-Key, nicht eindeutige URLs — siehe Eine Verbindung, viele Themen, um viele Sportarten, Ligen und Buchmacher über einen einzigen Socket abzudecken
  • 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 Streams fordern Sie eine maxStreams-Erhöhung pro Key an (ein kostenpflichtiges Add-on in jedem kostenpflichtigen Tarif) oder erstellen Sie einen separaten Key pro Prozess
  • Das Schließen der HTTP-Verbindung (oder der Aufruf von eventSource.close()) gibt den Slot sofort frei
  • Verwenden Sie breitere Filter auf weniger Streams anstelle vieler eingegrenzter Streams
  • Die Payload des connected-Events enthält Ihre stream_id zur Nachverfolgung

FehlerbehandlungPermalink for this section

Fehler auf Stream-EbenePermalink for this section

Als SSE-Events gesendete Fehler sind behebbar — die Verbindung bleibt offen:

event: error data: {"code":"upstream_error","message":"Temporary issue fetching data. Will retry."}

Fehler auf VerbindungsebenePermalink for this section

Diese schließen die Verbindung. Behandeln Sie sie in onerror:

FehlercodeHTTP-StatusBeschreibungLösung
too_many_streams429Zu viele gleichzeitige StreamsUngenutzte Streams schließen
tier_restricted403Streaming auf Ihrem Tarif nicht verfügbarWebSocket-Add-on hinzufügen
invalid_api_key401API key fehlt oder ist ungültigÜberprüfen Sie Ihren API key
validation_error400Ungültige FilterparameterQuery-Parameter überprüfen

Best PracticesPermalink for this section

  1. Den richtigen Channel verwenden — channel=odds nur für Quoten, channel=opportunities nur für Opportunities, channel=all für alles
  2. Filter zur Bandbreitenreduzierung verwenden — Übergeben Sie sport, league, sportsbook, market und event_id als Parameter, um die Daten einzugrenzen
  3. Schwellenwerte setzen — Verwenden Sie min_ev und min_profit, um Opportunities mit geringem Wert serverseitig herauszufiltern
  4. Auf snapshot:complete warten — Dies signalisiert, dass alle initialen Daten gesendet wurden. Blenden Sie Ladeanzeigen nach Empfang aus
  5. odds:removed behandeln — Entfernen Sie Quoten beim Empfang aus dem lokalen Status, um veraltete Daten zu vermeiden
  6. Wiederverbindung sauber handhaben — EventSource verbindet sich automatisch wieder, aber setzen Sie den lokalen Status zurück, wenn Sie ein neues snapshot-Event erhalten
  7. Updates asynchron verarbeiten — Blockieren Sie nicht den Event-Handler; stellen Sie Updates für die Hintergrundverarbeitung in eine Warteschlange
  8. Heartbeats überwachen — Wenn innerhalb von 60 Sekunden kein Heartbeat eintrifft, gilt die Verbindung als veraltet und sollte neu aufgebaut werden
  9. Ungenutzte Streams schließen — Jeder offene Stream zählt gegen Ihr Limit für gleichzeitige Streams
  10. Last-Event-ID verwenden — Ermöglicht es dem Server, verpasste Events nach einer Wiederverbindung erneut zu senden
  11. Opportunities per id upserten — Ein :detected-Eintrag kann ein Update einer Opportunity sein, die Sie bereits halten. Lösen Sie Alerts nur für eine id aus, die Sie noch nicht gesehen haben, und vergessen Sie die ids, die *:expired auflistet

Migration: Kompakte SSE-DeltasPermalink for this section

Breaking Change für SSE-odds:update-Konsumenten. Das odds:update-Event sendet jetzt kompakte OddsDelta-Objekte, die nur dynamische Felder enthalten (id, odds_american, odds_decimal, odds_probability, line, is_live, timestamp). Statische Felder wie sportsbook, sport, league, home_team, away_team, market_type, selection, deep_link und event_start_time werden nur im initialen snapshot-Event gesendet. Die Ausnahme ist eine Zeile mit einer id, die Sie noch nicht empfangen haben — ein Markt, der mitten im Stream geöffnet oder wieder geöffnet wurde —, die vollständig, mit allen Odds-Feldern, im selben odds:update-Event gesendet wird.

Hintergrund: Die vorherige Payload sendete bei jeder Änderung das vollständige Odds-Objekt und erzeugte ~170 KB/s pro Verbindung. Das kompakte Delta reduziert die Bandbreite um etwa das Fünffache, indem nur die 6-7 Felder gesendet werden, die sich tatsächlich geändert haben.

Was Sie in Ihrem Client ändern müssen:

  1. Snapshot-Quoten in einer lokalen Map nach id indiziert speichern. Das snapshot-Event sendet weiterhin vollständige Odds-Objekte mit allen Feldern. Eine odds:update-Zeile mit einer id, die nicht in der Map ist, ist selbst ein vollständiges Odds-Objekt — fügen Sie sie der Map hinzu.

  2. odds:update-Deltas anhand der id zusammenführen, statt sie als eigenständige Objekte zu behandeln. Jedes Delta enthält nur die Felder, die sich ändern können — schlagen Sie das vollständige Objekt in Ihrer lokalen Map nach und wenden Sie das Update an.

  3. Greifen Sie nicht auf statische Felder von Delta-Objekten zu. Felder wie event_id, market_type, selection, home_team und sportsbook sind in Deltas nicht vorhanden. Lesen Sie diese stattdessen aus Ihrer lokalen Map.

Vorher (fehlerhaft — Zugriff auf Felder, die nicht im Delta enthalten sind):

eventSource.addEventListener('odds:update', (e) => { const { odds } = JSON.parse(e.data); for (const o of odds) { // ❌ o.event_id, o.market_type, o.selection sind in Deltas undefined console.log(`${o.event_id} ${o.market_type}: ${o.selection} → ${o.odds_american}`); } });

Nachher (korrekt — in den lokalen Status zusammenführen):

eventSource.addEventListener('odds:update', (e) => { const { odds } = JSON.parse(e.data); for (const delta of odds) { const full = oddsMap.get(delta.id); if (full) { Object.assign(full, delta); // Geänderte Felder zusammenführen // ✅ full.event_id, full.market_type, full.selection sind weiterhin verfügbar console.log(`${full.event_id} ${full.market_type}: ${full.selection} → ${full.odds_american}`); } else { // Eine `id`, die Sie noch nicht halten, kommt als vollständige Zeile — so speichern oddsMap.set(delta.id, delta); } } });

Verwandte EndpointsPermalink for this section

Last updated on