Skip to Content
Referência da APIStream WebSocket

WebSocket Stream

wss://ws.sharpapi.io — Atualizações de odds e oportunidades em tempo real via WebSocket.

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

Uma descrição AsyncAPI 3.0 legível por máquina deste endpoint — canais, mensagens, schemas e bindings — está publicada em /asyncapi.yaml. Use-a para geração de código de SDK ou para alimentar ferramentas AsyncAPI como o Studio .

Por que WebSocket?Permalink for this section

WebSocket fornece uma conexão persistente e full-duplex. Em comparação com SSE:

RecursoSSE (/api/v1/stream)WebSocket (ws.sharpapi.io)
DireçãoApenas Servidor → ClienteBidirecional
ReconexãoAutomática (Last-Event-ID)Gerenciada pelo cliente
FiltrosDefinidos uma vez via parâmetros de queryAtualizados a qualquer momento via mensagem subscribe
ProtocoloStreaming HTTP/1.1WebSocket (RFC 6455)
Suporte a navegadoresEventSource nativoWebSocket nativo

Ambos os protocolos entregam os mesmos dados com a mesma latência. Escolha WebSocket quando precisar alterar filtros sem reconectar.

AutenticaçãoPermalink for this section

Passe sua API key como parâmetro de query na URL de conexão:

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

Você também pode passar filtros iniciais e assinaturas de canal como parâmetros de query:

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

Parâmetros de QueryPermalink for this section

ParâmetroTipoPadrãoDescrição
api_keystring—Obrigatório. Sua API key
channelsstringtodosAssina canais de dados específicos, separados por vírgula. Valores válidos: ev, arbitrage, middles, low_hold, odds. Omita para receber todos os dados permitidos pelo seu plano.
sportstringtodosFiltra por esporte(s), separados por vírgula (ex.: basketball, football, ice_hockey)
sportsbookstringconforme planoFiltra por sportsbook(s), separados por vírgula
leaguestringtodosFiltra por liga(s), separadas por vírgula
marketstringtodosFiltra por tipo(s) de mercado, separados por vírgula (ex.: moneyline, point_spread, total_points, player_points)
event_idstringtodosFiltra por ID(s) específicos de evento, separados por vírgula
min_evnumber2.0Percentual mínimo de EV para oportunidades de +EV
min_profitnumber0.5Percentual mínimo de lucro para oportunidades de arbitragem e low-hold
min_oddsnumber—Filtra odds pelo valor mínimo de odds americanas (ex.: -200)
max_oddsnumber—Filtra odds pelo valor máximo de odds americanas (ex.: 500)
statestring—Código de estado dos EUA para deep links de sportsbook em eventos de odds e oportunidades (ex.: nj, ny, il). Garante que URLs de deep_link redirecionem para o domínio do sportsbook específico do estado correto.
from_seqstring—Tenta replay de melhor esforço após este checkpoint opaco global_seq. Veja Reconexão com Replay.

Use canais para reduzir o tamanho do payload. Sem channels, o servidor envia todos os tipos de oportunidade mais o dump completo de odds. Se você precisa apenas de dados de low-hold, conecte com channels=low_hold para pular EV, arbitragem, middles e odds brutas inteiramente.

Ciclo de Vida da ConexãoPermalink for this section

Cliente Servidor | | |--- WS Upgrade ?api_key=xxx&channels=ev,odds →| | | Auth + obtém slot de stream |← connected ----------------------------------| Boas-vindas (plano, recursos, canais) |← subscribed ---------------------------------| Confirmação de filtros |← opportunities_snapshot (ev) ----------------| Oportunidades de EV |← initial (draftkings) -----------------------| Odds por sportsbook |← initial (fanduel) --------------------------| (em chunks por book) |← snapshot:complete --------------------------| Todos os dados iniciais enviados | | |← odds:update --------------------------------| Atualização incremental de odds |← ev:detected --------------------------------| Oportunidade de +EV (nova ou atualizada) |← heartbeat ----------------------------------| Keep-alive (a cada 30s) | | |--- { type: "ping" } → | |← pong ---------------------------------------| | | |--- { type: "subscribe", channels, filters } →| Atualiza canais/filtros |← subscribed ---------------------------------| Nova assinatura confirmada | | |--- close ----------------------------------→| Fechamento normal (1000)

Protocolo de MensagensPermalink for this section

Cliente → ServidorPermalink for this section

subscribe — Define ou atualiza canais e filtros. Enviado automaticamente na conexão se passado como parâmetros de query.

{ "type": "subscribe", "channels": ["ev", "odds"], "filters": { "sports": ["basketball"], "sportsbooks": ["draftkings", "fanduel"], "leagues": ["nba"], "markets": ["moneyline", "player_points"], "eventIds": ["32825-35775-2026-02-08"], "min_ev": 3.0, "min_profit": 1.5 } }
CampoTipoDescrição
channelsstring[]Opcional. Canais de dados a assinar: ev, arbitrage, middles, low_hold, odds. Omita para manter os canais atuais.
filters.sportsstring[]Opcional. Filtra por esporte(s): basketball, football, ice_hockey, baseball, soccer, etc.
filters.sportsbooksstring[]Opcional. Filtra por sportsbook(s).
filters.leaguesstring[]Opcional. Filtra por liga(s).
filters.marketsstring[]Opcional. Filtra por tipo(s) de mercado.
filters.eventIdsstring[]Opcional. Filtra por ID(s) específicos de evento.
filters.min_evnumberOpcional. Limite mínimo de percentual de EV (padrão 2.0).
filters.min_profitnumberOpcional. Percentual mínimo de lucro para arbitragem/low-hold (padrão 0.5).

ping — Keepalive. Envie a cada 25 segundos para evitar timeouts.

{ "type": "ping" }

resyncPermalink for this section

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

Reenvia o snapshot inicial para os canais aos quais você já está inscrito, no socket aberto. Suas inscrições e filtros permanecem inalterados, portanto nenhuma atualização é perdida em uma lacuna entre cancelar e reinscrever.

  • Confirmado com resync:started, seguido pela mesma sequência de snapshot que uma nova inscrição produz.
  • Somente canais já inscritos são aceitos — isso não é uma porta dos fundos para inscrição. Nomear qualquer canal que você não possui rejeita a mensagem inteira com channel_not_subscribed.
  • Limitado a uma por 10 segundos por conexão — uma recusa é resync_rate_limited com retry_after_ms, e uma solicitação em andamento é resync_in_progress. Um dump de snapshot é custoso; um dump ilimitado acionado pelo cliente seria uma negação de serviço autoinfligida.
  • O suporte é anunciado como features.client_resync no ack connected — detecte-o lá em vez de sondar.

resync_required é o sinal separado de servidor para cliente descrito em Ressincronização Completa; nunca o reenvie ao servidor.

Servidor → ClientePermalink for this section

connectedPermalink for this section

Enviado imediatamente após autenticação bem-sucedida.

{ "type": "connected", "seq": 12847, "message": "Welcome to SharpAPI real-time odds stream", "stream_id": "ws_mle3husw_ezoyvp", "tier": "pro", "features": { "ev": true, "arbitrage": true, "middles": true, "low_hold": true }, "channels": ["ev", "odds"], "global_seq": "12847", "books": { "max": -1, "allowed": null }, "streams": { "max": 1, "active": 1 }, "heartbeat_interval_ms": 30000, "pong_timeout_ms": 120000, "timestamp": "2026-02-08T18:47:17.559Z" }
CampoTipoDescrição
seqintegerRepresentação inteira legada do checkpoint global de processo. Nem todo frame de controle ou snapshot o inclui.
stream_idstringIdentificador único da conexão
tierstringSeu plano de assinatura
featuresobjectQuais tipos de oportunidade seu plano suporta
channelsstring[] | nullAssinaturas de canal ativas, ou null se estiver recebendo todos os dados permitidos pelo plano
global_seqstringCheckpoint em string decimal segura para JavaScript. Trate-o como opaco e armazene-o sem conversão numérica.
resumedbooleanEm uma tentativa com from_seq, indica se o replay foi aceito. Uma recusa inclui fallback_reason.
fallback_reasonstringPresente quando um resume solicitado é recusado; a conexão recebe então um snapshot autoritativo completo.
streamsobjectSeu limite de streams simultâneos por chave. max é autoritativo — é o mesmo valor que o servidor aplica. active é informativo: é contado por processo do servidor enquanto o limite é aplicado em toda a frota, portanto active < max não prova que sua próxima conexão não deslocará uma existente. Trate o fechamento 4001 independentemente.
heartbeat_interval_msintegerCadência do frame heartbeat da aplicação, em milissegundos. Dimensione seu watchdog de liveness a partir deste valor em vez de codificar um fixo — veja heartbeat.
pong_timeout_msintegerO prazo de leitura do servidor. Se nenhum PONG chegar dentro desta janela, a conexão é encerrada. Este é um prazo rígido, ao contrário do frame de heartbeat de melhor esforço.
books.maxintegerMáximo de sportsbooks permitidos para seu plano (-1 = ilimitado)
books.allowedstring[] | nullSportsbooks específicos permitidos, ou null para todos

Quando um resume solicitado é recusado, esse mesmo frame informa o desfecho explícito:

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

subscribedPermalink for this section

Confirma seus canais e filtros ativos.

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

opportunities_snapshotPermalink for this section

Snapshot de oportunidades para um único tipo de canal. Enviado uma vez por canal de oportunidade assinado durante a carga inicial de dados. Inclui apenas o tipo de oportunidade que você assinou.

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

A chave de nível superior corresponde ao tipo de canal: ev, arbitrage, middles ou low_hold. Cada mensagem de snapshot contém apenas um tipo. Snapshots grandes são automaticamente divididos em chunks — quando isso acontece, as mensagens incluem os campos chunk e totalChunks.

Todos os campos de oportunidade usam nomenclatura snake_case (ex.: event_id, market_type, profit_percent, detected_at). Isso se aplica de forma consistente em todos os canais, tipos de mensagem e protocolos (REST, SSE e WebSocket).

initialPermalink for this section

Snapshot de odds por sportsbook. Enviado uma vez por sportsbook quando o canal odds está assinado. Requer o canal odds.

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

As odds são divididas em chunks por sportsbook — você receberá uma mensagem initial por book. Books grandes podem ser divididos em múltiplas mensagens (cada frame é limitado a 256KB serializados). Se você não precisa de odds brutas, omita o canal odds para pular isso inteiramente.

snapshot:completePermalink for this section

Sinaliza o fim de um snapshot inicial, de um fallback de full resync ou de um replay bem-sucedido. É seguro ocultar estados de carregamento após recebê-lo. Um snapshot completo carrega books e total_odds. Quando um resume solicitado é recusado, ele carrega também mode: "full_resync" e o mesmo fallback_reason informado por connected:

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

Um replay normal ou consolidado aceito tem outra forma de conclusão:

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

Um snapshot novo comum omite mode e fallback_reason. A aceitação do replay é informada pelo frame connected anterior, não por este frame de conclusão.

CampoTipoDescrição
booksstring[]Lista de sportsbooks incluídos no snapshot inicial
total_oddsintegerTotal de linhas de odds enviadas no snapshot completo
modestringresume após um replay, ou full_resync após um resume explicitamente recusado. Um snapshot novo comum pode omiti-lo.
fallback_reasonstringPor que o checkpoint solicitado não pôde ser reproduzido. Corresponde ao motivo em connected.
replayed_countintegerReplay normal: frames em buffer enviados. Replay consolidado: linhas alteradas atuais enviadas.
skipped_countintegerFrames em buffer excluídos pela assinatura e pelos filtros ativos. O replay consolidado informa 0.
last_seqstringRecibo emitido pelo servidor para a varredura de replay concluída. Armazene-o apenas após receber este limite de conclusão.
gap_detectedbooleanEm um replay iniciado, true significa que o cliente deve reconciliar o estado em vez de tratar a conclusão como autoritativa.

odds:updatePermalink for this section

Atualização incremental de odds de um único sportsbook.

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

odds:removedPermalink for this section

Odds removidas por um sportsbook (ex.: mercado retirado, evento liquidado).

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

ev:detectedPermalink for this section

Nova oportunidade de +EV, ou uma versão atualizada de uma já enviada (mesmo id). Apenas plano Pro ou superior.

:detected significa nova ou atualizada — faça upsert por id. O stream WebSocket 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 — na mensagem normal ev:detected, arb:detected, middles:detected ou low_hold:detected. Armazene as oportunidades por id e substitua a versão armazenada a cada item. Alerte apenas sobre um id que você ainda não viu e esqueça um id quando *:expired o listar. O stream SSE se comporta da mesma forma desde 2026-09-26.

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

ev:expiredPermalink for this section

Oportunidade de +EV detectada anteriormente não está mais disponível.

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

arb:detectedPermalink for this section

Nova oportunidade de arbitragem, ou uma versão atualizada de uma já enviada (mesmo id). Apenas plano Hobby ou superior.

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

arb:expiredPermalink for this section

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

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

middles:detectedPermalink for this section

Nova oportunidade de middle, ou uma versão atualizada de uma já enviada (mesmo id). Requer o canal middles.

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

middles:expiredPermalink for this section

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

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

low_hold:detectedPermalink for this section

Nova oportunidade de low-hold, ou uma versão atualizada de uma já enviada (mesmo id). Requer o canal low_hold.

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

low_hold:expiredPermalink for this section

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

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

heartbeatPermalink for this section

Um frame de liveness em nível de aplicação, enviado em uma cadência fixa para cada conexão autenticada independentemente da atividade do mercado — continua chegando em um book noturno tranquilo. Seu intervalo é anunciado como heartbeat_interval_ms no ack connected (30 s hoje).

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

Ele carrega o valor de sequência atual, o que o torna útil além de liveness. Leia-o como uma verificação bilateral em vez de uma regra de uma linha, pois é um único contador compartilhado por cada canal e book na instância do servidor à qual você está conectado:

  • Avançar não significa que seus dados estão fluindo. Ele se move sempre que qualquer book emite conteúdo, portanto não pode revelar uma paralisação em um único book — um book silencioso enquanto os outros transmitem mantém o contador subindo.
  • Ficar parado nem sempre significa que algo está quebrado. É igualmente plano em um slate genuinamente tranquilo, como um jogo noturno sem nada se movendo.

Portanto, trate um global_seq plano como sinal de paralisação apenas quando suas próprias linhas também estiverem desatualizadas e você esperar atividade. Reconectar com um contador plano despeja um snapshot completo em um mercado tranquilo — a espiral descrita abaixo. Para liveness por book, rastreie quando você recebeu por último uma linha para cada book que lhe importa: o frame heartbeat do WebSocket não carrega timestamps por book.

Este é apenas um sinal de paralisação, nunca um ponto de controle de reconexão — veja Reconexão com Replay para o recibo seguro.

Este frame é de melhor esforço, e os dois temporizadores de 30 segundos têm contratos diferentes. O heartbeat é distribuído com um envio não bloqueante, portanto um cliente com contrapressão legitimamente perde um ciclo — e a condição que o descarta é sua própria lentidão, portanto um watchdog definido em um intervalo transforma uma paralisação transitória em uma reconexão espúria, que despeja um snapshot completo e piora a contrapressão.

O contrato rígido é o prazo de PONG do protocolo (pong_timeout_ms, 120 s hoje): se nenhum PONG chegar dentro dessa janela, o servidor encerra a conexão. Esse é um prazo de leitura, não um contador de batimentos perdidos.

Dimensione um watchdog de heartbeat a partir de ambos os valores anunciados:

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

que são 90 s nas cadências atuais. O min importa: um mero 3x pode exceder todo o orçamento de pong com uma cadência diferente. Os dois temporizadores têm fases independentes — o prazo de leitura é redefinido quando seu PONG chega, o heartbeat corre a partir de um ticker do servidor — portanto isso limita quanto tempo você espera, não garante que seu watchdog dispare antes do servidor fechar. Trate um fechamento de keepalive como uma reconexão, não como uma falha.

pongPermalink for this section

Resposta a um ping do cliente.

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

errorPermalink for this section

Notificação de erro. A conexão pode permanecer aberta (para erros não fatais) ou ser fechada (para erros de auth/limite).

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

A camada WebSocket emite um conjunto pequeno e fixo de códigos de erro em nível de frame para erros de protocolo do cliente. Eles são distintos dos códigos de erro HTTP retornados pelos endpoints REST.

CódigoSignificado
invalid_messageO frame não pôde ser parseado como JSON ou não correspondeu ao formato esperado
unknown_message_typeO campo type não é um dos valores auth, subscribe, filter, refresh_token, ping
missing_tokenO frame auth ou refresh_token não incluiu um campo token
missing_channelsO frame subscribe não incluiu um array channels não vazio
not_authenticatedEnviou subscribe, filter ou refresh_token antes que o auth fosse bem-sucedido
already_authenticatedO cliente enviou um segundo frame auth após o primeiro ter sido bem-sucedido

Os frames WebSocket também podem carregar os códigos no estilo HTTP invalid_api_key, tier_restricted e too_many_streams — esses fazem com que o servidor feche a conexão após o frame ser enviado. Veja Visão Geral da API → Códigos de Erro para a lista completa.

Códigos de FechamentoPermalink for this section

CódigoSignificadoResolução
1000Fechamento normalFechamento limpo iniciado pelo cliente ou servidor
1006Fechamento anormal (lado do cliente)Queda de rede ou kill do processo — sempre reconecte
1009Mensagem grande demais (lado do cliente)O limite de mensagem recebida da sua biblioteca está abaixo dos 256KB do frame de snapshot — aumente-o (ex.: max_size do websockets do Python) para pelo menos 512KB
4001Falha de autenticação ou deslocamento por uma sessão mais novaVerifique sua API key. Se o motivo do fechamento for displaced by newer session, outra conexão assumiu o único slot por chave — não reconecte automaticamente
4003As permissões mudaram durante o stream (downgrade, chave revogada, add-on removido)Reconecte para reautorizar com as permissões atuais. Se o plano realmente mudou, corrija isso primeiro — uma reconexão com chave revogada ou sem o add-on é recusada na autenticação

O código 1006 é reservado pela RFC 6455 e nunca é transmitido pela rede. Sua biblioteca WebSocket o gera localmente quando a conexão TCP é perdida sem um handshake de fechamento adequado (falha de rede, kill do processo, timeout no nível do SO). O servidor não o enviou. Sempre reconecte em caso de 1006.

Números de SequênciaPermalink for this section

global_seq é um checkpoint de string decimal seguro para JavaScript para replay de reconexão de melhor esforço. Frames de dados reproduzíveis também podem carregar seq, o mesmo checkpoint global de processo em uma representação inteira legada. Esses valores não são contadores por conexão e não são contíguos para um assinante filtrado: frames de outros canais, livros e assinantes também consomem valores.

Lacunas numéricas observadas, portanto, não indicam perda ou uma mensagem descartada. Reaja a sinais de recuperação explícitos — resync_required, gap_detected e um resume recusado — em vez de exigir continuidade N+1. Frames de snapshot e controle não carregam uniformemente nenhum dos campos de sequência. Dados reproduzidos retêm seu checkpoint original e adicionam "replay": true.

A entrega entre livros também pode reordenar valores globais de processo, portanto um cliente pode observar 101 antes de 100. Nunca calcule um checkpoint de reconexão a partir do último valor observado, um máximo acumulado, máximos por fonte ou continuidade aparente. Não existe checkpoint calculável pelo cliente que avance entre os recibos emitidos pelo servidor.

O recibo de avanço seguro é last_seq em um snapshot:complete recebido com mode: "resume". Em uma conexão nova ou fallback de ressincronização completa, connected.global_seq é um piso conservador, mas não confirme esse piso até que o próximo snapshot:complete fresco/completo confirme que a linha de base autoritativa foi totalmente recebida. heartbeat.global_seq não é seguro como recibo: pode refletir um valor cunhado antes da entrega. Preserve checkpoints seguros como strings decimais sem conversão numérica do JavaScript.

Reconexão com ReplayPermalink for this section

Para uma desconexão breve, reconecte com os mesmos canais e filtros mais o último recibo do servidor confirmado. from_seq sozinho solicita um replay local ao processo de melhor esforço:

wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&sport=basketball&sportsbook=draftkings,fanduel&league=nba&from_seq=12900
ParâmetroEfeito
from_seq=NTenta replay estritamente após o checkpoint N

Quando aceito, connected inclui "resumed": true; frames reproduzidos são marcados com "replay": true, e a fase de replay termina com snapshot:complete em "mode": "resume". O recebimento do last_seq desse frame de conclusão é o que faz avançar o checkpoint seguro. connected.global_seq em um resume aceito descreve o fim pretendido do replay, não a prova de que todos os frames de replay chegaram ao cliente. Um replay normal envia frames elegíveis estritamente após from_seq em ordem de buffer e recupera frames que chegaram durante essa varredura. Uma lacuna ampla somente de odds pode ser consolidada no estado atual de linhas modificadas em vez de cada transição intermediária. Se a consolidação não for elegível ou exceder seu limite de envio, o servidor explicitamente volta a um snapshot completo; não trunca o replay silenciosamente. Aplique atualizações e remoções de forma idempotente.

Quando o checkpoint não pode ser respeitado, o resultado é explícito: connected inclui "resumed": false e "fallback_reason", seguido de snapshots autoritativos e snapshot:complete com "mode": "full_resync". Os motivos atuais incluem parse_error, foreign_seq, process_restarted, seq_too_old, gap_too_large e disabled.

Se um replay começa mas depois termina com "gap_detected": true, descarte o checkpoint armazenado e reconcilie via REST ou obtenha um snapshot novo reconectando sem from_seq. A nova conexão envia um novo snapshot autoritativo.

from_seq é uma otimização de latência sobre o buffer de replay curto, não um mecanismo de completude. Não existe checkpoint calculável pelo cliente que avance entre os recibos do servidor. Use um snapshot completo ou reconciliação REST quando o estado completo for importante.

let resumeCheckpoint; let pendingSnapshotFloor; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'connected') { // Um piso fresco/de ressincronização completa só fica seguro após seu snapshot concluir. pendingSnapshotFloor = msg.resumed === true ? undefined : msg.global_seq; } if (msg.type === 'snapshot:complete' && msg.mode === 'resume') { if (msg.gap_detected) { // O servidor declarou esta varredura com lacuna: o checkpoint não é reutilizável. resumeCheckpoint = undefined; reconcileThroughRestOrRequestFreshSnapshot(); } else { // Recibo do servidor: todos os frames de replay antes deste limite foram entregues. resumeCheckpoint = msg.last_seq; } pendingSnapshotFloor = undefined; } else if ( msg.type === 'snapshot:complete' && (msg.mode === 'full_resync' || msg.mode === undefined) && pendingSnapshotFloor ) { // A linha de base autoritativa fresca/completa agora está concluída. resumeCheckpoint = pendingSnapshotFloor; pendingSnapshotFloor = undefined; } if (msg.replay) { console.log('Replayed event:', msg.type); } }; // On reconnect: function reconnect() { const params = new URLSearchParams({ api_key: 'YOUR_KEY', channels: 'ev,odds' }); if (resumeCheckpoint) params.set('from_seq', resumeCheckpoint); ws = new WebSocket(`wss://ws.sharpapi.io?${params}`); }

A retenção do replay é nominal e de melhor esforço, não uma duração garantida. Roteamento de processos, implantações, expiração baseada em tempo, expulsão de entradas/bytes e limites de replay podem reduzir a janela utilizável. resume_id durável não é suportado atualmente como checkpoint do cliente; frames de produção ao vivo o omitem. Preserve apenas os recibos de checkpoint emitidos pelo servidor descritos acima.

Conforme verificado em 2026-08-27, a produção executa o log durável em modo shadow somente de medição. Essa configuração de implantação com data não é uma promessa de que o resume durável está disponível.

Ressincronização CompletaPermalink for this section

Use qualquer um dos três caminhos de snapshot fresco suportados:

  1. Reconecte sem from_seq, depois restaure os canais e filtros pretendidos na URL de conexão ou em mensagens de inscrição. A nova conexão envia um snapshot completo imediatamente após a autenticação.
  2. Cancele a inscrição no canal afetado e inscreva-se novamente. A transição torna o canal novo e envia um snapshot fresco.
  3. Envie resync com os canais afetados no socket aberto — a inscrição nunca é descartada, portanto nada é perdido em uma janela de cancelamento. Veja resync.

Um subscribe duplicado ou uma atualização somente de filtros não aciona um snapshot.

O servidor envia resync_required quando a contrapressão descarta deltas ao vivo:

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

dropped é o número de frames descartados para sua conexão desde o resync_required anterior — não um total de vida útil, e não um valor de toda a frota. Está lá para que você possa dimensionar a lacuna: alguns frames em um mercado tranquilo é uma decisão diferente de várias centenas no meio de um slate.

O frame só é enviado quando essa contagem é diferente de zero, portanto recebê-lo sempre significa perda real. Recupere-se reconciliando via REST ou usando qualquer caminho de snapshot fresco acima; não reenvie resync_required ao servidor.

Exemplos de CódigoPermalink for this section

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

Limites de Streams ConcorrentesPermalink for this section

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

PlanoMáx. de streams concorrentes 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

Streams Concorrentes AdicionaisPermalink for this section

Rodar mais de um socket simultâneo em uma única chave — um par de failover hot/warm, ou uma conexão por worker na sua própria frota — é um aumento de capacidade provisionado na sua chave (um override maxStreams por chave), não uma mudança de código do seu lado. É um add-on pago em qualquer plano pago — fale com vendas informando quantos streams você precisa; entra em vigor na sua próxima conexão, sem rotação de chave e sem redeploy.

Confirme o que foi concedido lendo streams.max no ack de connected — ele informa o limite que é de fato aplicado, então você nunca precisa inferir seu limite a partir de um deslocamento:

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

streams.active conta os streams mantidos na instância que está te atendendo, enquanto o limite é aplicado em toda a frota. Portanto é um piso: active < max não garante que uma nova conexão deixará de deslocar uma das suas próprias sessões. Trate como diagnóstico (“estou prestes a derrubar minha outra sessão?”) e continue tratando 4001 de qualquer forma.

Criar uma chave separada por processo é a alternativa e não precisa de provisionamento — cada chave carrega o próprio slot.

Uma segunda conexão na mesma chave não é rejeitada — ela desloca a primeira. O novo socket sempre conecta e o antigo é fechado com 4001 displaced by newer session (“a mais nova vence”). O sinal equivalente no SSE é um evento final displaced com reconnect: false.

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.

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 sockets realmente paralelos, use Streams Concorrentes Adicionais acima. Veja Uma conexão, muitos tópicos para cobrir muitos esportes, ligas e casas em um único socket.

429 too_many_streams é retornado no upgrade HTTP 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 é recusada antes com 403 tier_restricted, antes do upgrade, e nunca chega ao limitador. Em um plano pago normal ocorre deslocamento.

Boas PráticasPermalink for this section

  1. Use canais — Assine apenas os dados que você precisa. channels=low_hold pula o dump completo de odds e outros tipos de oportunidade, reduzindo o payload inicial de megabytes para kilobytes
  2. Envie pings a cada 25 segundos — O servidor envia heartbeats a cada 30s, mas pings explícitos previnem timeouts de proxy/firewall
  3. Use filtros — Passe os parâmetros sport, sportsbook, league, market e event_id para restringir os dados dentro dos canais assinados
  4. Defina limites — Use min_ev e min_profit para filtrar oportunidades de baixo valor no servidor, reduzindo ruído
  5. Atualize via subscribe — Altere canais, filtros e limites sem reconectar
  6. Trate códigos de fechamento — 4001 significa chave inválida ou deslocamento por uma sessão mais nova (diferencie pelo motivo do fechamento), 4003 significa que as permissões mudaram durante o stream (downgrade, chave revogada, add-on removido) — reconecte para reautorizar, depois de checar que o plano ainda permite
  7. Acompanhe os recibos do servidor — Confirme snapshot:complete.last_seq após um resume bem-sucedido, ou o piso connected.global_seq pendente somente após seu snapshot fresco/completo concluir; nunca derive from_seq de dados arbitrários ou frames de heartbeat
  8. Implemente reconexão — Diferente do SSE, o WebSocket não reconecta automaticamente. Use backoff exponencial (1s, 2s, 4s, …) com replay via from_seq para curtas interrupções
  9. Aguarde snapshot:complete — Isso sinaliza que todos os dados iniciais foram enviados. Oculte estados de carregamento após recebê-lo
  10. Trate odds:removed — Remova odds do seu estado local quando receber esta mensagem para evitar mostrar dados desatualizados
  11. Feche conexões não utilizadas — Cada chave permite 1 stream concorrente por padrão; uma segunda conexão na mesma chave desloca a mais antiga (fechamento 4001)

RelacionadosPermalink for this section

Last updated on