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?
WebSocket fornece uma conexão persistente e full-duplex. Em comparação com SSE:
| Recurso | SSE (/api/v1/stream) | WebSocket (ws.sharpapi.io) |
|---|---|---|
| Direção | Apenas Servidor → Cliente | Bidirecional |
| Reconexão | Automática (Last-Event-ID) | Gerenciada pelo cliente |
| Filtros | Definidos uma vez via parâmetros de query | Atualizados a qualquer momento via mensagem subscribe |
| Protocolo | Streaming HTTP/1.1 | WebSocket (RFC 6455) |
| Suporte a navegadores | EventSource nativo | WebSocket nativo |
Ambos os protocolos entregam os mesmos dados com a mesma latência. Escolha WebSocket quando precisar alterar filtros sem reconectar.
Autenticação
Passe sua API key como parâmetro de query na URL de conexão:
wss://ws.sharpapi.io?api_key=sk_live_your_keyVocê 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=nbaParâmetros de Query
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
api_key | string | — | Obrigatório. Sua API key |
channels | string | todos | Assina 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. |
sport | string | todos | Filtra por esporte(s), separados por vírgula (ex.: basketball, football, ice_hockey) |
sportsbook | string | conforme plano | Filtra por sportsbook(s), separados por vírgula |
league | string | todos | Filtra por liga(s), separadas por vírgula |
market | string | todos | Filtra por tipo(s) de mercado, separados por vírgula (ex.: moneyline, point_spread, total_points, player_points) |
event_id | string | todos | Filtra por ID(s) específicos de evento, separados por vírgula |
min_ev | number | 2.0 | Percentual mínimo de EV para oportunidades de +EV |
min_profit | number | 0.5 | Percentual mínimo de lucro para oportunidades de arbitragem e low-hold |
min_odds | number | — | Filtra odds pelo valor mínimo de odds americanas (ex.: -200) |
max_odds | number | — | Filtra odds pelo valor máximo de odds americanas (ex.: 500) |
state | string | — | 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_seq | string | — | 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ão
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 Mensagens
Cliente → Servidor
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
}
}| Campo | Tipo | Descrição |
|---|---|---|
channels | string[] | Opcional. Canais de dados a assinar: ev, arbitrage, middles, low_hold, odds. Omita para manter os canais atuais. |
filters.sports | string[] | Opcional. Filtra por esporte(s): basketball, football, ice_hockey, baseball, soccer, etc. |
filters.sportsbooks | string[] | Opcional. Filtra por sportsbook(s). |
filters.leagues | string[] | Opcional. Filtra por liga(s). |
filters.markets | string[] | Opcional. Filtra por tipo(s) de mercado. |
filters.eventIds | string[] | Opcional. Filtra por ID(s) específicos de evento. |
filters.min_ev | number | Opcional. Limite mínimo de percentual de EV (padrão 2.0). |
filters.min_profit | number | Opcional. 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" }resync
{ "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_limitedcomretry_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_resyncno ackconnected— 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 → Cliente
connected
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"
}| Campo | Tipo | Descrição |
|---|---|---|
seq | integer | Representação inteira legada do checkpoint global de processo. Nem todo frame de controle ou snapshot o inclui. |
stream_id | string | Identificador único da conexão |
tier | string | Seu plano de assinatura |
features | object | Quais tipos de oportunidade seu plano suporta |
channels | string[] | null | Assinaturas de canal ativas, ou null se estiver recebendo todos os dados permitidos pelo plano |
global_seq | string | Checkpoint em string decimal segura para JavaScript. Trate-o como opaco e armazene-o sem conversão numérica. |
resumed | boolean | Em uma tentativa com from_seq, indica se o replay foi aceito. Uma recusa inclui fallback_reason. |
fallback_reason | string | Presente quando um resume solicitado é recusado; a conexão recebe então um snapshot autoritativo completo. |
streams | object | Seu 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_ms | integer | Cadê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_ms | integer | O 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.max | integer | Máximo de sportsbooks permitidos para seu plano (-1 = ilimitado) |
books.allowed | string[] | null | Sportsbooks 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"
}subscribed
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_snapshot
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).
initial
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:complete
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.
| Campo | Tipo | Descrição |
|---|---|---|
books | string[] | Lista de sportsbooks incluídos no snapshot inicial |
total_odds | integer | Total de linhas de odds enviadas no snapshot completo |
mode | string | resume após um replay, ou full_resync após um resume explicitamente recusado. Um snapshot novo comum pode omiti-lo. |
fallback_reason | string | Por que o checkpoint solicitado não pôde ser reproduzido. Corresponde ao motivo em connected. |
replayed_count | integer | Replay normal: frames em buffer enviados. Replay consolidado: linhas alteradas atuais enviadas. |
skipped_count | integer | Frames em buffer excluídos pela assinatura e pelos filtros ativos. O replay consolidado informa 0. |
last_seq | string | Recibo emitido pelo servidor para a varredura de replay concluída. Armazene-o apenas após receber este limite de conclusão. |
gap_detected | boolean | Em um replay iniciado, true significa que o cliente deve reconciliar o estado em vez de tratar a conclusão como autoritativa. |
odds:update
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:removed
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:detected
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:expired
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:detected
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:expired
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:detected
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:expired
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:detected
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:expired
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"
}heartbeat
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.
pong
Resposta a um ping do cliente.
{
"type": "pong",
"timestamp": "2026-02-08T18:47:42.000Z"
}error
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ódigo | Significado |
|---|---|
invalid_message | O frame não pôde ser parseado como JSON ou não correspondeu ao formato esperado |
unknown_message_type | O campo type não é um dos valores auth, subscribe, filter, refresh_token, ping |
missing_token | O frame auth ou refresh_token não incluiu um campo token |
missing_channels | O frame subscribe não incluiu um array channels não vazio |
not_authenticated | Enviou subscribe, filter ou refresh_token antes que o auth fosse bem-sucedido |
already_authenticated | O 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 Fechamento
| Código | Significado | Resolução |
|---|---|---|
1000 | Fechamento normal | Fechamento limpo iniciado pelo cliente ou servidor |
1006 | Fechamento anormal (lado do cliente) | Queda de rede ou kill do processo — sempre reconecte |
1009 | Mensagem 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 |
4001 | Falha de autenticação ou deslocamento por uma sessão mais nova | Verifique 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 |
4003 | As 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ência
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 Replay
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âmetro | Efeito |
|---|---|
from_seq=N | Tenta 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 Completa
Use qualquer um dos três caminhos de snapshot fresco suportados:
- 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. - Cancele a inscrição no canal afetado e inscreva-se novamente. A transição torna o canal novo e envia um snapshot fresco.
- Envie
resynccom os canais afetados no socket aberto — a inscrição nunca é descartada, portanto nada é perdido em uma janela de cancelamento. Vejaresync.
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ódigo
Browser
// 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 Concorrentes
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.
| Plano | Má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 Adicionais
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áticas
- Use canais — Assine apenas os dados que você precisa.
channels=low_holdpula o dump completo de odds e outros tipos de oportunidade, reduzindo o payload inicial de megabytes para kilobytes - Envie pings a cada 25 segundos — O servidor envia heartbeats a cada 30s, mas pings explícitos previnem timeouts de proxy/firewall
- Use filtros — Passe os parâmetros
sport,sportsbook,league,marketeevent_idpara restringir os dados dentro dos canais assinados - Defina limites — Use
min_evemin_profitpara filtrar oportunidades de baixo valor no servidor, reduzindo ruído - Atualize via
subscribe— Altere canais, filtros e limites sem reconectar - Trate códigos de fechamento —
4001significa chave inválida ou deslocamento por uma sessão mais nova (diferencie pelo motivo do fechamento),4003significa 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 - Acompanhe os recibos do servidor — Confirme
snapshot:complete.last_seqapós um resume bem-sucedido, ou o pisoconnected.global_seqpendente somente após seu snapshot fresco/completo concluir; nunca derivefrom_seqde dados arbitrários ou frames de heartbeat - Implemente reconexão — Diferente do SSE, o WebSocket não reconecta automaticamente. Use backoff exponencial (1s, 2s, 4s, …) com replay via
from_seqpara curtas interrupções - Aguarde
snapshot:complete— Isso sinaliza que todos os dados iniciais foram enviados. Oculte estados de carregamento após recebê-lo - Trate
odds:removed— Remova odds do seu estado local quando receber esta mensagem para evitar mostrar dados desatualizados - 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)
Relacionados
- Referência da API SSE Stream — Alternativa Server-Sent Events
- Visão Geral de Streaming — Conceitos e comparação
- Guia de Streaming WebSocket — Guia de introdução