Skip to Content

Stream Unificado

GET /api/v1/stream — Atualizações em tempo real de odds e oportunidades via Server-Sent Events (SSE).

Requer Add-on WebSocket ($99/mês) em qualquer plano pago, ou Enterprise (incluído). O plano gratuito não oferece suporte a streaming.

AutenticaçãoPermalink for this section

Passe sua API key via cabeçalho ou parâmetro de consulta:

# Cabeçalho (recomendado para uso server-side) curl -H "X-API-Key: sk_live_your_key" \ https://api.sharpapi.io/api/v1/stream # Parâmetro de consulta (obrigatório para EventSource no navegador) https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key

Parâmetros de ConsultaPermalink for this section

ParâmetroTipoPadrãoDescrição
channelstringopportunitiesO que transmitir: odds, opportunities, gamestate (apenas Enterprise) ou all. Também aceita o alias plural channels. Uma lista separada por vírgulas com mais de um canal colapsa para all — veja a nota abaixo.
sportstringtodosFiltrar por esporte(s), separados por vírgula (ex.: basketball, football, ice_hockey)
sportsbookstringpermitidos pelo planoFiltrar por sportsbook(s), separados por vírgula
leaguestringtodasFiltrar por liga(s), separadas por vírgula
event_idstringtodosFiltrar por ID(s) de evento, separados por vírgula
marketstringtodosFiltrar por tipo(s) de mercado, separados por vírgula (ex.: moneyline, point_spread, total_points, player_points)
min_evnumber2.0Porcentagem mínima de EV para eventos de oportunidade +EV
min_profitnumber0.5Porcentagem mínima de lucro apenas para eventos de arbitragem (não se aplica à filtragem de low-hold)
statestring—Código de estado dos EUA apenas para roteamento de deep links; não filtra nem altera odds. Aceita os 50 códigos de estados dos EUA e dc. Um código válido adiciona ?state= aos deep links de odds. Códigos omitidos, vazios ou não suportados não adicionam sufixo, então o redirecionamento aplica seu próprio padrão pa. Códigos não suportados emitem filter_warning após connected; a conexão é estabelecida.
api_keystring—API key (alternativa à autenticação por cabeçalho para EventSource no navegador)

Opções de ChannelPermalink for this section

ChannelEventos EntreguesCaso de Uso
oddssnapshot, odds:update, odds:removed, heartbeatAcompanhar movimentações de odds
opportunitiessnapshot, ev:detected/expired, arb:detected/expired, middles:detected/expired, low_hold:detected/expired, heartbeatAlertar sobre oportunidades
gamestategamestate:snapshot, gamestate:update, gamestate:final, heartbeatPlacares ao vivo, períodos, cronômetros e dados situacionais por evento. Cada gamestate:update reemite o slate completo do momento — no SSE não existe gamestate:removed (veja gamestate:update). Apenas plano Enterprise. Veja Live Game State para o catálogo completo de campos.
allTodos os tipos de eventoVisão completa em tempo real

Uma assinatura por stream SSE. Para manter paridade com a API WebSocket, o endpoint aceita tanto channel quanto o plural channels, e ambos toleram um valor separado por vírgulas. Mas uma conexão SSE mantém uma única assinatura: se você passar mais de um canal válido (ex.: ?channels=odds,opportunities), a requisição colapsa para channel=all em vez de retornar um erro. Um único valor (?channel=odds) transmite apenas aquele canal. Para assinar de forma seletiva um subconjunto específico de canais, use a API WebSocket, que oferece filtragem multicanal real com channels= em uma única conexão.

Rotas de ConveniênciaPermalink for this section

RotaEquivalente A
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

Tipos de Evento SSEPermalink for this section

connectedPermalink for this section

Enviado imediatamente quando o stream é estabelecido.

event: connected data: {"stream_id":"stream_1704960637000","channel":"all","filters":{"sportsbook":null,"sport":["basketball"],"league":["nba"],"event":null,"market":null},"reconnected":false}
CampoTipoDescrição
stream_idstringIdentificador único do stream
channelstringEco do channel solicitado (odds, opportunities ou all)
filtersobjectEco dos filtros ativos
reconnectedbooleantrue se esta for uma reconexão via Last-Event-ID — também num resume bem-sucedido, então não é o sinal para limpar o estado
resumedbooleanPresente quando você reconectou com um Last-Event-ID. true → o servidor está reenviando os eventos odds que você perdeu (com replayed_count) em vez de enviar um snapshot; se não conseguir concluir o reenvio, um snapshot completo vem mesmo assim, rotulado com mode: "full_resync" no snapshot:complete. false → o servidor não conseguiu retomar; um snapshot completo é enviado e fallback_reason diz por quê
replayed_countnumberCom resumed: true — quantos eventos em buffer são reenviados
fallback_reasonstringCom resumed: false — por que o resume recorreu ao snapshot (por exemplo seq_too_old, process_restarted, filter_changed, channel_unsupported). Informativo; a recuperação é a mesma: aceite o snapshot completo. Lista completa de valores
trialobject | undefinedPresente se o usuário estiver em um trial de streaming. Contém active, expires_at, remaining_hours, max_streams

snapshotPermalink for this section

Despejo completo de dados enviado após connected. Contém todas as odds ou oportunidades atuais que correspondem aos seus filtros. Conjuntos grandes são divididos em múltiplos eventos snapshot (até 1000 itens cada).

Cada objeto de odds no snapshot contém todos os campos — esta é a forma completa de Odds que seu cliente deve armazenar localmente. Eventos odds:update subsequentes enviam apenas os campos alterados (veja abaixo).

Nos channels opportunities e all, os fragmentos de oportunidades trazem seu array sob o tipo de oportunidade em vez de odds — ev, arbitrage, middles ou low_hold — com os mesmos campos count, total, offset e has_more. Armazene esses itens também por id: um ev:detected posterior (ou arb:/middles:/low_hold:detected) com o mesmo id é uma atualização de um deles.

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}
CampoTipoDescrição
oddsarrayArray de objetos Odds completos (veja endpoint Odds para todos os campos)
countnumberNúmero de odds neste fragmento
totalnumberNúmero total de odds que correspondem aos filtros
offsetnumberOffset deste fragmento no resultado completo
has_morebooleantrue se mais fragmentos snapshot virão a seguir

snapshot:completePermalink for this section

Sinaliza que todos os snapshots iniciais foram enviados. Seguro para ocultar estados de carregamento após recebê-lo.

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

odds:updatePermalink for this section

Disparado quando as odds mudam para um sportsbook. Enviado apenas nos channels odds ou all.

Payload delta compacto. Eventos delta contêm apenas campos que podem mudar entre atualizações — id, odds_american, odds_decimal, odds_probability, line, is_live e timestamp. Campos estáticos como sportsbook, sport, league, home_team, away_team, market_type, selection, deep_link e event_start_time não são incluídos nos deltas. Mescle cada delta no seu mapa local de odds por id usando os objetos completos recebidos no snapshot inicial. Algumas linhas chegam completas em vez de como delta: uma linha com um id que você ainda não tem é enviada completa, com todos os campos de Odds, no mesmo evento odds:update — armazene-a como está em vez de descartá-la. Veja Migração: Deltas SSE Compactos abaixo.

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}

Campos do objeto delta (OddsDelta):

CampoTipoDescrição
idstringID único da odd — corresponde ao id do snapshot inicial
odds_americannumberOdds americanas atualizadas (ex.: -150)
odds_decimalnumberOdds decimais atualizadas (ex.: 1.667)
odds_probabilitynumberProbabilidade implícita atualizada (ex.: 0.6)
linenumber | nullLinha/spread atualizada (ex.: -3.5), ou null para moneyline
is_livebooleanSe o evento está atualmente ao vivo
timestampstringHorário ISO 8601 em que a SharpAPI atualizou esta odd pela última vez através do seu pipeline — avança a cada ciclo de ingestão. É um sinal de frescor / liveness do feed; não é quando o preço mudou pela última vez. Veja Entendendo o campo timestamp.

Campos do envelope:

CampoTipoDescrição
oddsarrayArray de objetos OddsDelta (compactos — apenas campos dinâmicos)
countnumberNúmero de odds neste fragmento
bookstringSportsbook que sofreu alteração (ex.: "draftkings")
partialbooleantrue se mais fragmentos virão para este lote de atualização

ev:detectedPermalink for this section

Uma nova oportunidade de valor esperado positivo, ou uma versão atualizada de uma já enviada (mesmo id). Enviado apenas nos channels opportunities ou all.

:detected significa nova ou atualizada — faça upsert por id. Desde 2026-09-26, o stream SSE reenvia uma oportunidade cujo conteúdo mudou sob o mesmo id — um novo preço, EV%, probabilidade justa, odds das pernas, is_suspended ou quality_tier — no evento normal ev:detected, arb:detected, middles:detected ou low_hold:detected, como a API WebSocket faz. Antes dessa data, o stream SSE entregava essas atualizações de forma irregular — algumas mudanças de conteúdo chegavam ao cliente sob o mesmo id, a maioria não —, de modo que um cliente que ignorava um id repetido podia manter uma versão desatualizada até a oportunidade expirar ou o cliente se reconectar.

  • Armazene as oportunidades por id e substitua a versão armazenada a cada item de :detected.
  • Alerte apenas sobre um id que você ainda não viu. Um id conhecido é uma atualização, não uma nova oportunidade. Conte como vistos os ids dos fragmentos de snapshot e esqueça um id quando *:expired o listar.

Eventos *:expired e o formato do payload não mudam.

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"}

Os quatro eventos :detected compartilham este envelope:

CampoTipoDescrição
opportunitiesarrayOportunidades novas ou atualizadas. Faça upsert de cada uma por id
countnumberNúmero de itens em opportunities
typestringev, arbitrage, middles ou low_hold

ev:expiredPermalink for this section

Uma oportunidade +EV detectada anteriormente não está mais disponível. expired lista os ids a remover; count e type são como em ev:detected.

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

arb:detectedPermalink for this section

Uma nova oportunidade de arbitragem, ou uma versão atualizada de uma já enviada (mesmo id). Enviado apenas nos channels opportunities ou all.

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

Uma oportunidade de arbitragem detectada anteriormente não está mais disponível.

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

middles:detectedPermalink for this section

Uma nova oportunidade de middle, ou uma versão atualizada de uma já enviada (mesmo id). Enviado apenas nos channels opportunities ou all.

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

Uma oportunidade de middle detectada anteriormente não está mais disponível.

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

low_hold:detectedPermalink for this section

Uma nova oportunidade de low-hold, ou uma versão atualizada de uma já enviada (mesmo id). Enviado apenas nos channels opportunities ou all.

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

Uma oportunidade de low-hold detectada anteriormente não está mais disponível.

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

gamestate:snapshotPermalink for this section

Slate ao vivo completo do momento, enviado uma única vez após connected no channel gamestate (ou all). O payload é uma lista plana de linhas de evento — cada linha tem o mesmo formato de um evento REST de Live Game State, mais o 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

Disparado a cada ciclo de atualização do gamestate. Enviado apenas nos channels gamestate ou all.

Reemissão do slate completo — não é um delta. Diferente de odds:update, cada gamestate:update por SSE carrega o slate ao vivo completo que corresponde aos seus filtros. Não existe evento gamestate:removed no SSE — um evento finalizado continua no payload como sua linha status: "final" durante a janela de carry e depois deixa de aparecer. Substitua seu gamestate local por inteiro a cada gamestate:update; não faça merge como delta, ou eventos encerrados ficarão no seu estado para sempre. Um caso limite: com filtros sport/league definidos, um ciclo que não casa com nenhum evento não envia nenhum gamestate:update (apenas streams sem filtro recebem o payload vazio {"data": []}), então o último evento encerrado nunca é substituído — se os heartbeats continuam mas as atualizações param, trate o slate como possivelmente vazio e expire as linhas não atualizadas. Se você precisa de entrega incremental — atualizações de linhas alteradas mais listas explícitas de ids gamestate:removed — use a API WebSocket em vez disso; os formatos dos campos estão na referência de Live Game State.

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}]}

O channel gamestate também não é retomável no SSE: o replay por Last-Event-ID cobre apenas o channel odds. Uma reconexão de gamestate sempre reinicializa com um gamestate:snapshot novo — trate cada reconexão como um começo do zero: limpe o gamestate local e reconstrua a partir do novo snapshot. Note que os frames de gamestate (e os heartbeats) não carregam linha id: de SSE, então um EventSource simples no channel gamestate reconecta sem Last-Event-ID — o connected dele não traz nem resumed nem fallback_reason. Essas chaves aparecem apenas quando a reconexão de fato apresenta um Last-Event-ID (por exemplo no channel all, onde eventos de odds definem o cursor de retomada, ou com um header definido à mão): o servidor então confirma com resumed: false e fallback_reason: "channel_unsupported".

gamestate:finalPermalink for this section

Enviado uma vez quando um evento termina, com sua linha terminal (status: "final", placar final, completed_at). Mesmo envelope {"data": [...]}, mesmo acesso e mesmos filtros sport/league do gamestate:update, sem linha id:. A linha também continua em cada gamestate:update durante a janela de carry, então este frame é um aviso, não a única entrega. Pode se repetir ocasionalmente — deduplique por (event_id, completed_at). Veja Avisos de evento finalizado.

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

Disparado quando um mercado é suspenso/fechado (ex.: após um gol, durante um movimento de linha ou em um bloqueio no fim do jogo) — o preço fica congelado, mas a seleção não é mais apostável. Carrega o subconjunto suspenso do delta atual, com o mesmo formato de payload de odds:update, com is_active: false. Enviado apenas nos channels odds ou all.

É um sinal de bloqueio dedicado e é complementar — as mesmas linhas também chegam em odds:update com is_active: false, então clientes que já leem is_active não precisam assinar odds:locked separadamente. Use quando quiser um sinal de bloqueio dedicado sem fazer parse de cada odds:update.

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}

Um mercado que reabre emite um odds:update normal com is_active: true (e um preço novo). Mercados que uma casa remove por completo chegam por odds:removed.

O envelope é o mesmo de odds:update, incluindo o campo replay: frames odds:locked reenviados durante uma retomada carregam "replay": true, exatamente como frames odds:update e odds:removed reenviados.

odds:removedPermalink for this section

Odds removidas por um sportsbook (ex.: mercado retirado, evento liquidado). Enviado apenas nos channels odds ou all.

event: odds:removed id: evt_00051 data: {"ids":["123456","789012"],"count":2,"book":"draftkings"}
CampoTipoDescrição
idsstring[]IDs de odds a serem removidos do estado local
countnumberNúmero de odds removidas
bookstringSportsbook que removeu as odds

heartbeatPermalink for this section

Keep-alive enviado a cada 30 segundos. Se você não receber um heartbeat dentro de 60 segundos, a conexão pode estar obsoleta.

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

resync_requiredPermalink for this section

Enviado quando deltas ao vivo foram pulados porque seu cliente consumiu devagar demais (veja a política de consumidor lento). Entregue assim que a contrapressão passa, antes do próximo evento de dados — seu estado local está agora incompleto. Busque /api/v1/odds novamente para o escopo dos seus filtros, ou reconecte para um snapshot novo.

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

Enviado como evento final quando uma conexão mais nova com a mesma API key assume o slot do stream (a mais nova vence — veja Uma conexão, muitos tópicos); o stream é fechado em seguida. reconnect: false é determinante: não reconecte automaticamente, ou você vai derrubar sua própria sessão mais nova em loop.

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"}

O hint é o texto literal do servidor. O aumento a que ele se refere é um override maxStreams por chave, vendido como add-on pago em qualquer plano pago — veja Limites de Streams Concorrentes.

errorPermalink for this section

Erro recuperável no stream. A conexão permanece aberta.

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

ReconexãoPermalink for this section

SSE oferece suporte a reconexão automática via cabeçalho Last-Event-ID. No canal odds o servidor resolve um de dois resultados, e ele sempre diz qual:

  1. Resume (connected com resumed: true) — os eventos odds que você perdeu são reenviados a partir de uma janela best-effort, cada um marcado com "replay": true; nenhum snapshot é enviado. Mantenha seu estado local — os deltas reenviados o deixam atualizado. Um snapshot:complete com mode: "resume" marca a passagem para dados ao vivo.
  2. Ressincronização completa (connected com resumed: false e um fallback_reason) — o servidor não conseguiu reenviar. Um novo snapshot completo chega, e seu snapshot:complete traz mode: "full_resync".

resumed: true significa que a repetição começou, não que terminou: se depois chegarem chunks de snapshot, era o caminho 2 no fim — limpe seu estado local no primeiro desses chunks. Se nenhum chunk de snapshot chegar porque nada mais corresponde aos seus filtros, limpe-o quando snapshot:complete informar mode: "full_resync". Só eventos odds carregam linhas id: — reconexões em gamestate, opportunities e all sempre seguem o caminho do snapshot completo.

// Navegadores lidam com isso automaticamente com EventSource. // Para clientes personalizados, defina o cabeçalho na reconexão: const headers = { 'X-API-Key': 'YOUR_KEY', 'Last-Event-ID': 'evt_00042' };

Limpe seu estado local só quando um snapshot completo for chegar: num connected sem resumed: true; no primeiro chunk de snapshot depois de um resumed: true; ou, quando um snapshot completo depois de um resumed: true não traz nenhum chunk de snapshot, no snapshot:complete que informa mode: "full_resync". Não limpe em toda reconexão nem em reconnected: true — ele também é true num resume bem-sucedido, e lá nenhum snapshot chega para você mesclar os deltas. Se você não limpar antes de um snapshot completo de verdade, odds obsoletas da sessão anterior se misturarão com dados novos.

O EventSource do navegador lida com Last-Event-ID e a reconexão automaticamente. Nenhum código extra é necessário para a reconexão em si, mas você deve cuidar da limpeza de estado no lado do cliente.

Exemplos de CódigoPermalink for this section

// Mapa local de odds — indexado pelo ID da odd, armazena objetos Odds completos do snapshot. // Eventos delta são mesclados neste mapa pelo ID. const oddsMap = new Map(); let resuming = false; // definido por `connected` com `resumed: true` // Oportunidades por tipo, cada uma indexada por `id`. Um item de `:detected` é uma // oportunidade nova OU uma versão atualizada de uma que você já tem (mesmo `id`). const oppsByType = { ev: new Map(), arbitrage: new Map(), middles: new Map(), low_hold: new Map() }; // Upsert por `id`. Retorna true apenas na primeira vez que um `id` aparece — // alerte nisso, não em cada item de `:detected`. function upsertOpp(type, opp) { const isNew = !oppsByType[type].has(opp.id); oppsByType[type].set(opp.id, opp); // substitui a versão que você tinha 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); // Mantenha as odds locais só num resume (`resumed: true`): aí os deltas perdidos são // reenviados em vez de um snapshot. Qualquer outra conexão é seguida de um snapshot // completo — limpe agora. Não use `reconnected`: ele também é `true` num resume. resuming = resumed === true; if (!resuming) oddsMap.clear(); // Oportunidades nunca são retomadas: cada conexão as reenvia no `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); // Um snapshot depois de `resumed: true` significa que o servidor desistiu do resume: // este snapshot substitui seu estado. if (resuming) { oddsMap.clear(); resuming = false; } // Fragmentos de oportunidades trazem `ev` / `arbitrage` / `middles` / `low_hold` em vez de `odds` for (const type of Object.keys(oppsByType)) { for (const opp of data[type] ?? []) oppsByType[type].set(opp.id, opp); } if (!data.odds) return; // Armazena objetos Odds completos indexados por ID 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" ou ausente numa primeira conexão // Um snapshot completo pode chegar sem nenhum chunk de `snapshot` — nada mais // corresponde aos seus filtros —, então a limpeza no handler de `snapshot` nunca rodou. // A checagem de `resuming` evita apagar um snapshot que você acabou de guardar. 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); // Mescla deltas compactos no estado local — apenas campos dinâmicos são enviados for (const delta of odds) { const existing = oddsMap.get(delta.id); if (existing) { Object.assign(existing, delta); // Mescla campos alterados } else { // Um `id` que você ainda não tem chega como linha completa — guarde como está 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`); }); // Cada item de `:detected` é salvo com upsert; só um `id` visto pela primeira vez é registrado como novo. 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` lista os ids que sumiram. Esqueça-os, para que um `id` que // volte depois conte como novo de novo. 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...'); };

Limites de Streams ConcorrentesPermalink for this section

O limite é por chave de API e é compartilhado entre SSE e WebSocket. Não é por URL de conexão: uma segunda conexão com filtros diferentes não ganha um slot próprio.

PlanoStreams concorrentes máximos por chave
Qualquer plano pago (streaming via add-on WebSocket, $99/mês)1
Qualquer plano pago com Streams Concorrentes Adicionais provisionados (um override maxStreams por chave)O número provisionado na chave — continua sendo 1 até o override ser concedido; entre em contato com vendas

Abrir um segundo stream na mesma chave não retorna erro — ele desloca o primeiro. A nova conexão sempre é aceita e a antiga é fechada (“a mais nova vence”).

O lado deslocado é avisado explicitamente:

  • SSE — um evento final displaced com reconnect: false e, em seguida, o encerramento
  • WebSocket — código de fechamento 4001 displaced by newer session

Um cliente deslocado não deve reconectar automaticamente. O slot agora pertence à sessão mais nova, então reconectar a expulsaria imediatamente e criaria um loop de reconexão.

No SSE isso exige uma ação explícita: um EventSource nativo reconecta sozinho após qualquer encerramento pelo servidor, e reconnect: false no payload do evento não impede isso — esse campo é uma orientação para o seu código, não uma instrução para o navegador. Chame eventSource.close() dentro do seu handler displaced.

429 too_many_streams ainda é retornado, mas apenas quando uma chave que JÁ tem acesso a streaming resolve para zero slots — um override explícito maxStreams: 0. Uma chave sem acesso a streaming nem chega ao limitador: é recusada antes com 403 tier_restricted. Em um plano pago normal ocorre deslocamento, não um 429.

Gerenciando StreamsPermalink for this section

  • São contadas conexões abertas por chave de API, não URLs únicas — veja Uma conexão, muitos tópicos para cobrir muitos esportes, ligas e casas em um único socket
  • O limite é coordenado entre instâncias da API — a posse do slot fica em estado compartilhado, não por processo — então distribuir conexões por vários hosts próprios não é uma forma admitida de contorná-lo
  • Para streams realmente paralelos, solicite um aumento de maxStreams por chave (um add-on pago em qualquer tier pago) ou crie uma chave separada por processo
  • Fechar a conexão HTTP (ou chamar eventSource.close()) libera o slot imediatamente
  • Use filtros mais amplos em menos streams ao invés de muitos streams restritos
  • O payload do evento connected inclui seu stream_id para rastreamento

Tratamento de ErrosPermalink for this section

Erros de Nível de StreamPermalink for this section

Erros enviados como eventos SSE são recuperáveis — a conexão permanece aberta:

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

Erros de Nível de ConexãoPermalink for this section

Estes encerram a conexão. Trate-os no onerror:

Código de ErroStatus HTTPDescriçãoResolução
too_many_streams429Muitos streams concorrentesFeche streams não utilizados
tier_restricted403Streaming não disponível no seu planoAdicione o add-on WebSocket
invalid_api_key401API key ausente ou inválidaVerifique sua API key
validation_error400Parâmetros de filtro inválidosVerifique os parâmetros de consulta

Boas PráticasPermalink for this section

  1. Use o channel correto — channel=odds apenas para odds, channel=opportunities apenas para oportunidades, channel=all para tudo
  2. Use filtros para reduzir o consumo de banda — Passe os parâmetros sport, league, sportsbook, market e event_id para restringir os dados
  3. Defina limiares — Use min_ev e min_profit para filtrar oportunidades de baixo valor no servidor
  4. Aguarde por snapshot:complete — Isso sinaliza que todos os dados iniciais foram enviados. Oculte os estados de carregamento após recebê-lo
  5. Trate odds:removed — Remova odds do estado local quando recebidas para evitar exibir dados obsoletos
  6. Trate a reconexão de forma elegante — O EventSource se reconecta automaticamente, mas redefina o estado local quando receber um novo evento snapshot
  7. Processe atualizações de forma assíncrona — Não bloqueie o handler de eventos; enfileire as atualizações para processamento em segundo plano
  8. Monitore os heartbeats — Se nenhum heartbeat chegar em 60 segundos, considere a conexão obsoleta e reconecte
  9. Feche streams não utilizados — Cada stream aberto conta contra seu limite concorrente
  10. Use Last-Event-ID — Permite que o servidor reproduza eventos perdidos após uma reconexão
  11. Faça upsert das oportunidades por id — Um item de :detected pode ser uma atualização de uma oportunidade que você já tem. Alerte apenas sobre um id que você ainda não viu e esqueça os ids listados em *:expired

Migração: Deltas SSE CompactosPermalink for this section

Mudança incompatível para consumidores de odds:update SSE. O evento odds:update agora envia objetos OddsDelta compactos contendo apenas campos dinâmicos (id, odds_american, odds_decimal, odds_probability, line, is_live, timestamp). Campos estáticos como sportsbook, sport, league, home_team, away_team, market_type, selection, deep_link e event_start_time são enviados apenas no evento snapshot inicial. A exceção é uma linha com um id que você não recebeu antes — um mercado que abriu ou reabriu no meio do stream — que é enviada completa, com todos os campos de Odds, no mesmo evento odds:update.

Por quê: O payload anterior enviava o objeto Odds completo a cada alteração, gerando ~170 KB/s por conexão. O delta compacto reduz o consumo de banda em ~5x, enviando apenas os 6-7 campos que realmente mudaram.

O que alterar no seu cliente:

  1. Armazene as odds do snapshot em um mapa local indexado por id. O evento snapshot ainda envia objetos Odds completos com todos os campos. Uma linha de odds:update com um id que não está no mapa é ela mesma um objeto Odds completo — adicione-a ao mapa.

  2. Mescle os deltas de odds:update por id ao invés de tratá-los como objetos independentes. Cada delta contém apenas os campos que podem mudar — procure o objeto completo no seu mapa local e aplique a atualização.

  3. Não acesse campos estáticos em objetos delta. Campos como event_id, market_type, selection, home_team e sportsbook não estão presentes nos deltas. Leia-os do seu mapa local em vez disso.

Antes (incorreto — acessando campos não presentes no delta):

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

Depois (correto — mescla no estado local):

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); // Mescla campos alterados // ✅ full.event_id, full.market_type, full.selection ainda estão disponíveis console.log(`${full.event_id} ${full.market_type}: ${full.selection} → ${full.odds_american}`); } else { // Um `id` que você ainda não tem chega como linha completa — guarde como está oddsMap.set(delta.id, delta); } } });

Endpoints RelacionadosPermalink for this section

Last updated on