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.
Authentifizierung
Ü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_keyQuery-Parameter
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
channel | string | opportunities | Was 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. |
sport | string | alle | Filter nach Sportart(en), kommagetrennt (z. B. basketball, football, ice_hockey) |
sportsbook | string | tariferlaubt | Filter nach Sportsbook(s), kommagetrennt |
league | string | alle | Filter nach Liga(en), kommagetrennt |
event_id | string | alle | Filter nach Event-ID(s), kommagetrennt |
market | string | alle | Filter nach Markttyp(en), kommagetrennt (z. B. moneyline, point_spread, total_points, player_points) |
min_ev | number | 2.0 | Minimaler EV-Prozentsatz für +EV-Opportunity-Events |
min_profit | number | 0.5 | Minimaler Gewinnprozentsatz nur für Arbitrage-Events (gilt nicht für die Low-Hold-Filterung) |
state | string | — | 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_key | string | — | API key (Alternative zur Header-Authentifizierung für Browser-EventSource) |
Channel-Optionen
| Channel | Übermittelte Events | Anwendungsfall |
|---|---|---|
odds | snapshot, odds:update, odds:removed, heartbeat | Quotenbewegungen verfolgen |
opportunities | snapshot, ev:detected/expired, arb:detected/expired, middles:detected/expired, low_hold:detected/expired, heartbeat | Bei Opportunities benachrichtigen |
gamestate | gamestate:snapshot, gamestate:update, gamestate:final, heartbeat | Live-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. |
all | Alle Event-Typen | Vollstä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-Routen
| Route | Entspricht |
|---|---|
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-Typen
connected
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}| Feld | Typ | Beschreibung |
|---|---|---|
stream_id | string | Eindeutige Stream-Kennung |
channel | string | Echo des angeforderten Channels (odds, opportunities oder all) |
filters | object | Echo der aktiven Filter |
reconnected | boolean | true, wenn es sich um eine Wiederverbindung über Last-Event-ID handelt — auch bei einem erfolgreichen Resume, daher kein Signal zum Leeren des Status |
resumed | boolean | Vorhanden, 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_count | number | Bei resumed: true — wie viele gepufferte Events erneut gesendet werden |
fallback_reason | string | Bei 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 |
trial | object | undefined | Vorhanden, wenn der Benutzer sich in einer Streaming-Testphase befindet. Enthält active, expires_at, remaining_hours, max_streams |
snapshot
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}| Feld | Typ | Beschreibung |
|---|---|---|
odds | array | Array vollständiger Odds-Objekte (siehe Odds-Endpoint für alle Felder) |
count | number | Anzahl der Quoten in diesem Chunk |
total | number | Gesamtzahl der Quoten, die zu den Filtern passen |
offset | number | Offset dieses Chunks im Gesamtergebnis |
has_more | boolean | true, wenn weitere snapshot-Chunks folgen |
snapshot:complete
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:update
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):
| Feld | Typ | Beschreibung |
|---|---|---|
id | string | Eindeutige Quoten-ID — entspricht der id aus dem initialen Snapshot |
odds_american | number | Aktualisierte amerikanische Quote (z. B. -150) |
odds_decimal | number | Aktualisierte dezimale Quote (z. B. 1.667) |
odds_probability | number | Aktualisierte implizite Wahrscheinlichkeit (z. B. 0.6) |
line | number | null | Aktualisierte Line/Spread (z. B. -3.5) oder null für Moneyline |
is_live | boolean | Ob das Event derzeit live ist |
timestamp | string | ISO-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:
| Feld | Typ | Beschreibung |
|---|---|---|
odds | array | Array von OddsDelta-Objekten (kompakt — nur dynamische Felder) |
count | number | Anzahl der Quoten in diesem Chunk |
book | string | Sportsbook, das sich geändert hat (z. B. "draftkings") |
partial | boolean | true, wenn weitere Chunks für diesen Update-Batch folgen |
ev:detected
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
idund ersetzen Sie die gespeicherte Version durch jeden:detected-Eintrag. - Lösen Sie Alerts nur für eine
idaus, die Sie noch nicht gesehen haben. Eine bekannteidist ein Update, keine neue Opportunity. Zählen Sie die ids aus densnapshot-Chunks als gesehen, und vergessen Sie eineid, sobald*:expiredsie 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:
| Feld | Typ | Beschreibung |
|---|---|---|
opportunities | array | Neue oder aktualisierte Opportunities. Jeden Eintrag nach id einfügen oder ersetzen (Upsert) |
count | number | Anzahl der Einträge in opportunities |
type | string | ev, arbitrage, middles oder low_hold |
ev:expired
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:detected
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:expired
Eine zuvor erkannte Arbitrage-Opportunity ist nicht mehr verfügbar.
event: arb:expired
data: {"expired":["61c501b83ce932d1"],"count":1,"type":"arbitrage"}middles:detected
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:expired
Eine zuvor erkannte Middle-Opportunity ist nicht mehr verfügbar.
event: middles:expired
data: {"expired":["middle_abc123"],"count":1,"type":"middles"}low_hold:detected
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:expired
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:snapshot
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:update
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:final
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:locked
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:removed
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"}| Feld | Typ | Beschreibung |
|---|---|---|
ids | string[] | Quoten-IDs, die aus dem lokalen Status entfernt werden sollen |
count | number | Anzahl der entfernten Quoten |
book | string | Sportsbook, das die Quoten entfernt hat |
heartbeat
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_required
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."}displaced
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.
error
Behebbarer Fehler im Stream. Die Verbindung bleibt geöffnet.
event: error
data: {"code":"upstream_error","message":"Temporary issue fetching DraftKings data. Will retry."}Wiederverbindung
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:
- Resume (
connectedmitresumed: true) — die verpasstenodds-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:completemitmode: "resume"markiert den Übergang zu Live-Daten. - Vollständiger Resync (
connectedmitresumed: falseund einemfallback_reason) — der Server konnte nicht erneut senden. Ein frischer vollständiger Snapshot folgt, und seinsnapshot:completeträgtmode: "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.
Codebeispiele
Browser
// 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 Streams
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.
| Tarif | Max. 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 mitreconnect: 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 verwalten
- 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 Ihrestream_idzur Nachverfolgung
Fehlerbehandlung
Fehler auf Stream-Ebene
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 Verbindungsebene
Diese schließen die Verbindung. Behandeln Sie sie in onerror:
| Fehlercode | HTTP-Status | Beschreibung | Lösung |
|---|---|---|---|
too_many_streams | 429 | Zu viele gleichzeitige Streams | Ungenutzte Streams schließen |
tier_restricted | 403 | Streaming auf Ihrem Tarif nicht verfügbar | WebSocket-Add-on hinzufügen |
invalid_api_key | 401 | API key fehlt oder ist ungültig | Überprüfen Sie Ihren API key |
validation_error | 400 | Ungültige Filterparameter | Query-Parameter überprüfen |
Best Practices
- Den richtigen Channel verwenden —
channel=oddsnur für Quoten,channel=opportunitiesnur für Opportunities,channel=allfür alles - Filter zur Bandbreitenreduzierung verwenden — Übergeben Sie
sport,league,sportsbook,marketundevent_idals Parameter, um die Daten einzugrenzen - Schwellenwerte setzen — Verwenden Sie
min_evundmin_profit, um Opportunities mit geringem Wert serverseitig herauszufiltern - Auf
snapshot:completewarten — Dies signalisiert, dass alle initialen Daten gesendet wurden. Blenden Sie Ladeanzeigen nach Empfang aus odds:removedbehandeln — Entfernen Sie Quoten beim Empfang aus dem lokalen Status, um veraltete Daten zu vermeiden- Wiederverbindung sauber handhaben —
EventSourceverbindet sich automatisch wieder, aber setzen Sie den lokalen Status zurück, wenn Sie ein neuessnapshot-Event erhalten - Updates asynchron verarbeiten — Blockieren Sie nicht den Event-Handler; stellen Sie Updates für die Hintergrundverarbeitung in eine Warteschlange
- Heartbeats überwachen — Wenn innerhalb von 60 Sekunden kein Heartbeat eintrifft, gilt die Verbindung als veraltet und sollte neu aufgebaut werden
- Ungenutzte Streams schließen — Jeder offene Stream zählt gegen Ihr Limit für gleichzeitige Streams
Last-Event-IDverwenden — Ermöglicht es dem Server, verpasste Events nach einer Wiederverbindung erneut zu senden- Opportunities per
idupserten — Ein:detected-Eintrag kann ein Update einer Opportunity sein, die Sie bereits halten. Lösen Sie Alerts nur für eineidaus, die Sie noch nicht gesehen haben, und vergessen Sie die ids, die*:expiredauflistet
Migration: Kompakte SSE-Deltas
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:
-
Snapshot-Quoten in einer lokalen Map nach
idindiziert speichern. Dassnapshot-Event sendet weiterhin vollständigeOdds-Objekte mit allen Feldern. Eineodds:update-Zeile mit einerid, die nicht in der Map ist, ist selbst ein vollständigesOdds-Objekt — fügen Sie sie der Map hinzu. -
odds:update-Deltas anhand deridzusammenfü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. -
Greifen Sie nicht auf statische Felder von Delta-Objekten zu. Felder wie
event_id,market_type,selection,home_teamundsportsbooksind 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 Endpoints
- +EV-Opportunities - REST-Endpoint für EV-Daten (gestreamt über
ev:detected) - Arbitrage-Opportunities - REST-Endpoint für Arbs (gestreamt über
arb:detected) - Low-Hold-Opportunities - REST-Endpoint für Low Hold (gestreamt über
low_hold:detected) - Middles-Übersicht - Aggregierte Middle-Statistiken für Dashboard-Polling
- WebSocket-API - Bidirektionale Alternative zu SSE