Skip to Content
Referencia de la APIStream WebSocket

Stream WebSocket

wss://ws.sharpapi.io — Actualizaciones de cuotas y oportunidades en tiempo real mediante WebSocket.

Requiere el complemento WebSocket (99 $/mes) en cualquier plan de pago, o Enterprise (incluido). El plan Free no admite streaming.

Una descripción AsyncAPI 3.0 legible por máquina de este endpoint — canales, mensajes, esquemas y bindings — está publicada en /asyncapi.yaml. Úsala para la generación de código de SDK o para impulsar herramientas AsyncAPI como Studio .

¿Por qué WebSocket?Permalink for this section

WebSocket proporciona una conexión persistente y full-duplex. En comparación con SSE:

CaracterísticaSSE (/api/v1/stream)WebSocket (ws.sharpapi.io)
DirecciónSolo Servidor → ClienteBidireccional
ReconexiónAutomática (Last-Event-ID)Gestionada por el cliente
FiltrosEstablecidos una vez vía parámetros de consultaActualizables en cualquier momento mediante el mensaje subscribe
ProtocoloStreaming HTTP/1.1WebSocket (RFC 6455)
Soporte del navegadorEventSource nativoWebSocket nativo

Ambos protocolos entregan los mismos datos con la misma latencia. Elige WebSocket cuando necesites cambiar filtros sin reconectar.

AutenticaciónPermalink for this section

Pasa tu API key como parámetro de consulta en la URL de conexión:

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

También puedes pasar filtros iniciales y suscripciones a canales como parámetros de consulta:

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

Parámetros de consultaPermalink for this section

ParámetroTipoPor defectoDescripción
api_keystring—Obligatorio. Tu API key
channelsstringallSuscripción a canales de datos específicos, separados por comas. Valores válidos: ev, arbitrage, middles, low_hold, odds. Omite para recibir todos los datos permitidos por tu plan.
sportstringallFiltrar por deporte(s), separados por comas (p. ej. basketball, football, ice_hockey)
sportsbookstringtier-allowedFiltrar por sportsbook(s), separados por comas
leaguestringallFiltrar por liga(s), separadas por comas
marketstringallFiltrar por tipo(s) de mercado, separados por comas (p. ej. moneyline, point_spread, total_points, player_points)
event_idstringallFiltrar por ID(s) de evento específicos, separados por comas
min_evnumber2.0Porcentaje mínimo de EV para oportunidades +EV
min_profitnumber0.5Porcentaje mínimo de beneficio para oportunidades de arbitraje y low-hold
min_oddsnumber—Filtrar cuotas por valor mínimo de cuota americana (p. ej., -200)
max_oddsnumber—Filtrar cuotas por valor máximo de cuota americana (p. ej., 500)
statestring—Código de estado de EE. UU. para los deep links de sportsbooks en eventos de cuotas y oportunidades (p. ej., nj, ny, il). Garantiza que las URL deep_link redirijan al dominio del sportsbook específico del estado.
from_seqstring—Intenta replay de mejor esfuerzo después de este checkpoint opaco global_seq. Consulta Reconexión con Replay.

Usa canales para reducir el tamaño del payload. Sin channels, el servidor envía todos los tipos de oportunidades más el volcado completo de cuotas. Si solo necesitas datos low-hold, conéctate con channels=low_hold para omitir EV, arbitraje, middles y cuotas brutas por completo.

Ciclo de vida de la conexiónPermalink for this section

Cliente Servidor | | |--- WS Upgrade ?api_key=xxx&channels=ev,odds →| | | Auth + adquirir slot de stream |← connected ----------------------------------| Bienvenida (tier, features, channels) |← subscribed ---------------------------------| Confirmación de filtros |← opportunities_snapshot (ev) ----------------| Oportunidades EV |← initial (draftkings) -----------------------| Cuotas por sportsbook |← initial (fanduel) --------------------------| (fragmentadas por book) |← snapshot:complete --------------------------| Todos los datos iniciales enviados | | |← odds:update --------------------------------| Actualización incremental de cuotas |← ev:detected --------------------------------| Oportunidad +EV (nueva o actualizada) |← heartbeat ----------------------------------| Keep-alive (cada 30s) | | |--- { type: "ping" } → | |← pong ---------------------------------------| | | |--- { type: "subscribe", channels, filters } →| Actualizar canales/filtros |← subscribed ---------------------------------| Nueva suscripción confirmada | | |--- close ----------------------------------→| Cierre normal (1000)

Protocolo de mensajesPermalink for this section

Cliente → ServidorPermalink for this section

subscribe — Establece o actualiza canales y filtros. Se envía automáticamente en la conexión si se pasa como parámetros de consulta.

{ "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 } }
CampoTipoDescripción
channelsstring[]Opcional. Canales de datos a los que suscribirse: ev, arbitrage, middles, low_hold, odds. Omite para mantener los canales actuales.
filters.sportsstring[]Opcional. Filtrar por deporte(s): basketball, football, ice_hockey, baseball, soccer, etc.
filters.sportsbooksstring[]Opcional. Filtrar por sportsbook(s).
filters.leaguesstring[]Opcional. Filtrar por liga(s).
filters.marketsstring[]Opcional. Filtrar por tipo(s) de mercado.
filters.eventIdsstring[]Opcional. Filtrar por ID(s) de evento específicos.
filters.min_evnumberOpcional. Umbral mínimo de porcentaje de EV (por defecto 2.0).
filters.min_profitnumberOpcional. Porcentaje mínimo de beneficio para arbitraje/low-hold (por defecto 0.5).

ping — Keepalive. Envía cada 25 segundos para evitar timeouts.

{ "type": "ping" }

resyncPermalink for this section

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

Reenvía el snapshot inicial para los canales a los que ya está suscrito, en el socket abierto. Sus suscripciones y filtros permanecen intactos, por lo que no se pierde ninguna actualización en el intervalo entre cancelar y volver a suscribirse.

  • Confirmado con resync:started, seguido de la misma secuencia de snapshot que produce una nueva suscripción.
  • Solo se aceptan canales ya suscritos — esto no es una suscripción encubierta. Nombrar cualquier canal que no tenga rechaza todo el mensaje con channel_not_subscribed.
  • Limitado a una por cada 10 segundos por conexión — un rechazo es resync_rate_limited con retry_after_ms, y una solicitud en curso es resync_in_progress. Un volcado de snapshot es costoso; un volcado ilimitado impulsado por el cliente sería una denegación de servicio autoinfligida.
  • La compatibilidad se anuncia como features.client_resync en el ack connected — detéctela allí en lugar de sondearlo.

resync_required es la señal separada de servidor a cliente descrita en Resincronización Completa; nunca la reenvíe al servidor.

Servidor → ClientePermalink for this section

connectedPermalink for this section

Se envía inmediatamente tras una autenticación exitosa.

{ "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" }
CampoTipoDescripción
seqintegerRepresentación entera heredada del checkpoint global de proceso. No todos los frames de control o snapshot lo incluyen.
stream_idstringIdentificador único de conexión
tierstringTu plan de suscripción
featuresobjectQué tipos de oportunidades admite tu plan
channelsstring[] | nullSuscripciones a canales activas, o null si recibe todos los datos permitidos por el plan
global_seqstringCheckpoint en cadena decimal segura para JavaScript. Trátalo como opaco y almacénalo sin conversión numérica.
resumedbooleanEn un intento con from_seq, indica si se aceptó el replay. Un rechazo incluye fallback_reason.
fallback_reasonstringPresente cuando se rechaza un resume solicitado; la conexión recibe entonces un snapshot autoritativo completo.
streamsobjectSu límite de streams concurrentes por clave. max es autoritativo — es el mismo valor que el servidor aplica. active es informativo: se cuenta por proceso del servidor mientras el límite se aplica a nivel de flota, por lo que active < max no demuestra que su próxima conexión no desplazará a una existente. Maneje el cierre 4001 independientemente.
heartbeat_interval_msintegerCadencia del frame heartbeat de la aplicación, en milisegundos. Dimensione su watchdog de liveness a partir de este valor en lugar de codificar uno fijo — vea heartbeat.
pong_timeout_msintegerEl plazo de lectura del servidor. Si no llega ningún PONG dentro de esta ventana, la conexión se cierra. Este es un plazo estricto, a diferencia del frame de heartbeat de mejor esfuerzo.
books.maxintegerSportsbooks máximos permitidos para tu plan (-1 = ilimitado)
books.allowedstring[] | nullSportsbooks específicos permitidos, o null para todos

Cuando se rechaza un resume solicitado, ese mismo frame informa el desenlace explícito:

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

subscribedPermalink for this section

Confirma tus canales y filtros activos.

{ "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 un único tipo de canal. Se envía una vez por canal de oportunidades suscrito durante la carga inicial de datos. Solo incluye el tipo de oportunidad al que te has suscrito.

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

La clave de nivel superior coincide con el tipo de canal: ev, arbitrage, middles o low_hold. Cada mensaje de snapshot contiene solo un tipo. Los snapshots grandes se fragmentan automáticamente — cuando esto ocurre, los mensajes incluyen los campos chunk y totalChunks.

Todos los campos de oportunidad usan nomenclatura snake_case (p. ej. event_id, market_type, profit_percent, detected_at). Esto se aplica de forma consistente en todos los canales, tipos de mensaje y protocolos (REST, SSE y WebSocket).

initialPermalink for this section

Snapshot de cuotas por sportsbook. Se envía una vez por sportsbook cuando el canal odds está suscrito. Requiere el canal odds.

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

Las cuotas se fragmentan por sportsbook — recibirás un mensaje initial por book. Los books grandes pueden dividirse en varios mensajes (cada frame está limitado a 256KB serializados). Si no necesitas las cuotas brutas, omite el canal odds para saltarlas por completo.

snapshot:completePermalink for this section

Indica el final de un snapshot inicial, de un fallback de full resync o de un replay exitoso. Es seguro ocultar los estados de carga tras recibirlo. Un snapshot completo lleva books y total_odds. Cuando se rechaza un resume solicitado, lleva además mode: "full_resync" y el mismo fallback_reason que informó connected:

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

Un replay normal o consolidado aceptado tiene otra forma de finalización:

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

Un snapshot nuevo ordinario omite mode y fallback_reason. La aceptación del replay la informa el frame connected anterior, no este frame de finalización.

CampoTipoDescripción
booksstring[]Lista de sportsbooks incluidos en el snapshot inicial
total_oddsintegerTotal de filas de cuotas enviadas en el snapshot completo
modestringresume tras un replay, o full_resync tras un resume rechazado explícitamente. Un snapshot nuevo ordinario puede omitirlo.
fallback_reasonstringPor qué no se pudo reproducir el checkpoint solicitado. Coincide con el motivo de connected.
replayed_countintegerReplay normal: frames almacenados enviados. Replay consolidado: filas cambiadas actuales enviadas.
skipped_countintegerFrames almacenados excluidos por la suscripción y los filtros activos. El replay consolidado informa 0.
last_seqstringAcuse emitido por el servidor para el recorrido de replay completado. Almacénalo solo tras recibir este límite de finalización.
gap_detectedbooleanEn un replay iniciado, true significa que el cliente debe reconciliar el estado en lugar de tratar la finalización como autoritativa.

odds:updatePermalink for this section

Actualización incremental de cuotas desde un ú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

Cuotas eliminadas por un sportsbook (p. ej. mercado retirado, evento finalizado).

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

Nueva oportunidad +EV, o una versión actualizada de una ya enviada (mismo id). Solo plan Pro o superior.

:detected significa nueva o actualizada — haz upsert por id. El stream WebSocket reenvía una oportunidad cuyo contenido cambió bajo el mismo id — un nuevo precio, EV%, probabilidad justa, cuotas de las patas, is_suspended o quality_tier — en el mensaje habitual ev:detected, arb:detected, middles:detected o low_hold:detected. Guarda las oportunidades por id y reemplaza la versión guardada con cada elemento. Alerta solo sobre un id que no hayas visto y olvida un id cuando *:expired lo incluya. El stream SSE se comporta igual desde el 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

Una oportunidad +EV detectada anteriormente ya no está disponible.

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

Nueva oportunidad de arbitraje, o una versión actualizada de una ya enviada (mismo id). Solo plan Hobby o 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

Una oportunidad de arbitraje detectada anteriormente ya no está disponible.

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

Nueva oportunidad de middle, o una versión actualizada de una ya enviada (mismo id). Requiere el 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

Una oportunidad de middle detectada anteriormente ya no está disponible.

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

low_hold:detectedPermalink for this section

Nueva oportunidad de low-hold, o una versión actualizada de una ya enviada (mismo id). Requiere el 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

Una oportunidad de low-hold detectada anteriormente ya no está disponible.

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

heartbeatPermalink for this section

Un frame de liveness a nivel de aplicación, enviado en una cadencia fija a cada conexión autenticada independientemente de la actividad del mercado — sigue llegando en un libro nocturno tranquilo. Su intervalo se anuncia como heartbeat_interval_ms en el ack connected (30 s hoy).

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

Lleva el valor de secuencia actual, lo que lo hace útil más allá de la liveness. Léalo como una verificación bilateral en lugar de una regla de una línea, ya que es un único contador compartido por cada canal y libro en la instancia del servidor a la que está conectado:

  • Que avance no significa que sus datos estén fluyendo. Se mueve cuando cualquier libro emite contenido, por lo que no puede revelar un estancamiento en un único libro — un libro silencioso mientras los demás transmiten mantiene el contador subiendo.
  • Que se detenga no siempre significa que algo está roto. Es igualmente plano en una pizarra genuinamente tranquila, como un partido nocturno donde nada se mueve.

Trate un global_seq plano como señal de estancamiento solo cuando sus propias filas también estén desactualizadas y espere actividad. Reconectarse solo por un contador plano vuelca un snapshot completo en un mercado tranquilo — la espiral descrita a continuación. Para liveness por libro, rastree cuándo recibió por última vez una fila para cada libro que le importa: el frame heartbeat de WebSocket no lleva marcas de tiempo por libro.

Esta es solo una señal de estancamiento, nunca un punto de control de reconexión — vea Reconexión con Replay para el recibo seguro.

Este frame es de mejor esfuerzo, y los dos temporizadores de 30 segundos tienen contratos diferentes. El heartbeat se distribuye con un envío no bloqueante, por lo que un cliente con contrapresión legítimamente pierde un ciclo — y la condición que lo descarta es su propia lentitud, por lo que un watchdog establecido en un intervalo convierte un estancamiento transitorio en una reconexión espuria, lo que vuelca un snapshot completo y empeora la contrapresión.

El contrato estricto es el plazo de PONG del protocolo (pong_timeout_ms, 120 s hoy): si no llega ningún PONG dentro de esa ventana, el servidor cierra la conexión. Ese es un plazo de lectura, no un contador de latidos perdidos.

Dimensione un watchdog de heartbeat a partir de ambos valores anunciados:

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

que son 90 s con las cadencias actuales. El min importa: un mero 3x puede exceder todo el presupuesto de pong con una cadencia diferente. Los dos temporizadores están en fases independientes — el plazo de lectura se restablece cuando llega su PONG, el heartbeat corre desde un ticker del servidor — por lo que esto limita cuánto tiempo espera, no garantiza que su watchdog se active antes de que el servidor cierre. Trate un cierre de keepalive como una reconexión, no como un error.

pongPermalink for this section

Respuesta a un ping del cliente.

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

errorPermalink for this section

Notificación de error. La conexión puede permanecer abierta (para errores no fatales) o cerrarse (para errores de autenticación/límite).

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

La capa WebSocket emite un conjunto pequeño y fijo de códigos de error a nivel de frame para errores de protocolo del cliente. Son distintos de los códigos de error HTTP devueltos por los endpoints REST.

CódigoSignificado
invalid_messageEl frame no se pudo parsear como JSON o no coincidía con el formato esperado
unknown_message_typeEl campo type no es uno de auth, subscribe, filter, refresh_token, ping
missing_tokenEl frame auth o refresh_token no incluía un campo token
missing_channelsEl frame subscribe no incluía un array channels no vacío
not_authenticatedSe envió subscribe, filter o refresh_token antes de que auth tuviera éxito
already_authenticatedEl cliente envió un segundo frame auth después de que el primero tuviera éxito

Los frames WebSocket también pueden transportar los códigos al estilo HTTP invalid_api_key, tier_restricted y too_many_streams — estos hacen que el servidor cierre la conexión después de enviar el frame. Consulta Visión general de la API → Códigos de error para la lista completa.

Códigos de cierrePermalink for this section

CódigoSignificadoResolución
1000Cierre normalCierre limpio iniciado por el cliente o el servidor
1006Cierre anormal (lado cliente)Caída de red o terminación del proceso — reconectar siempre
1009Mensaje demasiado grande (lado cliente)El límite de tamaño de mensaje entrante de tu librería está por debajo de los 256KB del frame de snapshot — súbelo (p. ej. max_size de Python websockets) a 512KB como mínimo
4001Fallo de autenticación o desplazamiento por una sesión más recienteComprueba tu API key. Si el motivo de cierre es displaced by newer session, otra conexión ocupó el único slot por clave — no reconectes automáticamente
4003Los permisos cambiaron durante el stream (bajada de plan, clave revocada, complemento eliminado)Reconecta para reautorizar con los permisos actuales. Si el plan cambió de verdad, corrígelo primero — una reconexión con una clave revocada o sin el complemento se rechaza en la autenticación

El código 1006 está reservado por RFC 6455 y nunca se transmite por la red. Tu librería WebSocket lo genera localmente cuando se pierde la conexión TCP sin un handshake de cierre adecuado (fallo de red, terminación del proceso, timeout a nivel de SO). El servidor no lo envió. Reconecta siempre ante un 1006.

Números de secuenciaPermalink for this section

global_seq es un checkpoint de cadena decimal seguro para JavaScript para el replay de reconexión de mejor esfuerzo. Los frames de datos reproducibles también pueden llevar seq, el mismo checkpoint global de proceso en una representación entera heredada. Estos valores no son contadores por conexión y no son contiguos para un suscriptor filtrado: los frames de otros canales, libros y suscriptores también consumen valores.

Las brechas numéricas observadas, por tanto, no indican pérdida o un mensaje descartado. Reacciona a las señales de recuperación explícitas — resync_required, gap_detected y un resume rechazado — en lugar de requerir continuidad N+1. Los frames de snapshot y control no contienen uniformemente ninguno de los campos de secuencia. Los datos reproducidos conservan su checkpoint original y añaden "replay": true.

La entrega entre libros también puede reordenar los valores globales del proceso, por lo que un cliente puede observar 101 antes de 100. Nunca calcules un checkpoint de reconexión a partir del último valor observado, un máximo acumulado, máximos por fuente o continuidad aparente. No existe ningún checkpoint calculable por el cliente que avance entre los recibos emitidos por el servidor.

El recibo de avance seguro es last_seq en un snapshot:complete recibido con mode: "resume". En una conexión nueva o fallback de resincronización completa, connected.global_seq es un piso conservador, pero no confirmes ese piso hasta que el siguiente snapshot:complete fresco/completo confirme que la línea base autoritativa fue recibida íntegramente. heartbeat.global_seq no es seguro como recibo: puede reflejar un valor acuñado antes de la entrega. Conserva los checkpoints seguros como cadenas decimales sin conversión numérica de JavaScript.

Reconexión con ReplayPermalink for this section

Para una desconexión breve, reconecta con los mismos canales y filtros más el último recibo del servidor confirmado. from_seq solo solicita un replay local al proceso de mejor esfuerzo:

wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&sport=basketball&sportsbook=draftkings,fanduel&league=nba&from_seq=12900
ParámetroEfecto
from_seq=NIntenta replay estrictamente después del checkpoint N

Cuando se acepta, connected incluye "resumed": true; los frames reproducidos están marcados con "replay": true, y la fase de replay termina con snapshot:complete en "mode": "resume". La recepción del last_seq de ese frame de finalización es lo que hace avanzar el checkpoint seguro. connected.global_seq en un resume aceptado describe el fin previsto del replay, no la prueba de que todos los frames de replay llegaron al cliente. Un replay normal envía los frames elegibles estrictamente después de from_seq en orden de buffer y recupera los frames que llegaron durante ese recorrido. Una brecha amplia solo de cuotas puede consolidarse en cambio al estado actual de filas modificadas en lugar de cada transición intermedia. Si la consolidación no es elegible o supera su límite de envío, el servidor recurre explícitamente a un snapshot completo; no trunca el replay silenciosamente. Aplica actualizaciones y eliminaciones de forma idempotente.

Cuando el checkpoint no puede ser respetado, el resultado es explícito: connected incluye "resumed": false y "fallback_reason", seguido de snapshots autoritativos y snapshot:complete con "mode": "full_resync". Los motivos actuales incluyen parse_error, foreign_seq, process_restarted, seq_too_old, gap_too_large y disabled.

Si un replay comienza pero después termina con "gap_detected": true, descarta el checkpoint almacenado y reconcilia mediante REST u obtén un snapshot nuevo reconectando sin from_seq. La conexión nueva envía un nuevo snapshot autoritativo.

from_seq es una optimización de latencia sobre el buffer de replay corto, no un mecanismo de completitud. No existe ningún checkpoint calculable por el cliente que avance entre los recibos del servidor. Usa un snapshot completo o reconciliación REST cuando el estado completo sea importante.

let resumeCheckpoint; let pendingSnapshotFloor; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'connected') { // Un piso fresco/de resincronización completa solo es seguro tras completar su snapshot. pendingSnapshotFloor = msg.resumed === true ? undefined : msg.global_seq; } if (msg.type === 'snapshot:complete' && msg.mode === 'resume') { if (msg.gap_detected) { // El servidor declaró este recorrido con hueco: el checkpoint no es reutilizable. resumeCheckpoint = undefined; reconcileThroughRestOrRequestFreshSnapshot(); } else { // Recibo del servidor: todos los frames de replay antes de este límite fueron entregados. resumeCheckpoint = msg.last_seq; } pendingSnapshotFloor = undefined; } else if ( msg.type === 'snapshot:complete' && (msg.mode === 'full_resync' || msg.mode === undefined) && pendingSnapshotFloor ) { // La línea base autoritativa fresca/completa ahora está terminada. 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}`); }

La retención del replay es nominal y de mejor esfuerzo, no una duración garantizada. El enrutamiento de procesos, los despliegues, el vencimiento basado en tiempo, la expulsión de entradas/bytes y los límites de replay pueden acortar la ventana utilizable. El resume_id duradero no está soportado actualmente como checkpoint del cliente; los frames de producción en vivo lo omiten. Conserva únicamente los recibos de checkpoint emitidos por el servidor descritos anteriormente.

Según lo verificado el 2026-08-27, la producción ejecuta el log duradero en modo shadow solo de medición. Esta configuración de despliegue con fecha no es una promesa de que el resume duradero esté disponible.

Resincronización CompletaPermalink for this section

Use cualquiera de los tres métodos de snapshot fresco admitidos:

  1. Reconéctese sin from_seq, luego restaure los canales y filtros previstos en la URL de conexión o en los mensajes de suscripción. La nueva conexión envía un snapshot completo inmediatamente después de la autenticación.
  2. Cancele la suscripción al canal afectado y vuelva a suscribirse. La transición hace que el canal sea nuevo y envía un snapshot fresco.
  3. Envíe resync con los canales afectados en el socket abierto — la suscripción nunca se interrumpe, por lo que nada se pierde en una ventana de cancelación. Vea resync.

Un subscribe duplicado o una actualización solo de filtros no activa un snapshot.

El servidor envía resync_required cuando la contrapresión descarta deltas en 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 es el número de frames descartados para su conexión desde el resync_required anterior — no un total de por vida, y no una cifra de toda la flota. Está ahí para que pueda dimensionar la brecha: unos pocos frames en un mercado tranquilo es una decisión diferente a varios cientos en medio de una pizarra activa.

El frame solo se envía cuando ese recuento es mayor que cero, por lo que recibirlo siempre significa pérdida real. Recupérese conciliando a través de REST o usando cualquier método de snapshot fresco anterior; no reenvíe resync_required al servidor.

Ejemplos 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 } })); }

Límites de streams concurrentesPermalink for this section

El límite es por clave de API y se comparte entre WebSocket y SSE. No es por URL de conexión: un segundo socket con canales distintos no obtiene su propio slot.

PlanMáx. streams concurrentes por clave
Cualquier plan de pago (streaming mediante el complemento WebSocket, 99 $/mes)1
Cualquier plan de pago con Streams Concurrentes Adicionales aprovisionados (un override maxStreams por clave)El número aprovisionado en la clave

Streams Concurrentes AdicionalesPermalink for this section

Ejecutar más de un socket simultáneo sobre una sola clave — un par de failover hot/warm, o una conexión por worker en tu propia flota — es un aumento de capacidad aprovisionado sobre tu clave (un override maxStreams por clave), no un cambio de código de tu lado. Es un add-on de pago en cualquier plan de pago — contacta con ventas indicando cuántos streams necesitas; surte efecto en tu siguiente conexión, sin rotación de clave y sin redespliegue.

Confirma lo que te concedieron leyendo streams.max en el ack de connected — informa del límite que realmente se aplica, así nunca tienes que deducir tu límite a partir de un desplazamiento:

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

streams.active cuenta los streams retenidos en la instancia que te atiende, mientras que el límite se aplica en toda la flota. Por tanto es una cota inferior: active < max no garantiza que una nueva conexión evite desplazar a una de tus propias sesiones. Trátalo como un diagnóstico («¿estoy a punto de expulsar mi otra sesión?») y sigue gestionando 4001 de todos modos.

Acuñar una clave separada por proceso es la alternativa y no necesita aprovisionamiento — cada clave lleva su propio slot.

Una segunda conexión con la misma clave no se rechaza — desplaza a la primera. El nuevo socket siempre conecta y el anterior se cierra con 4001 displaced by newer session («gana la más reciente»). La señal equivalente en SSE es un evento final displaced con reconnect: false.

Un cliente desplazado no debe reconectarse automáticamente. El slot lo ocupa ahora la sesión más reciente, así que reconectar la expulsaría de inmediato y provocaría un bucle de reconexión.

El límite se coordina entre instancias de la API — la propiedad del slot se replica en un estado compartido, no por proceso — así que repartir las conexiones entre varios hosts propios no es una forma admitida de saltárselo. Para sockets realmente paralelos, usa Streams Concurrentes Adicionales más arriba. Consulta Una conexión, muchos temas para cubrir muchos deportes, ligas y casas en un solo socket.

429 too_many_streams se devuelve en el upgrade HTTP solo cuando una clave que YA tiene acceso al streaming se resuelve a cero slots — un override explícito maxStreams: 0. Una clave sin acceso al streaming se rechaza antes con 403 tier_restricted, previo al upgrade, y nunca llega al limitador. En un plan de pago normal se produce desplazamiento.

Buenas prácticasPermalink for this section

  1. Usa canales — Suscríbete solo a los datos que necesitas. channels=low_hold omite todo el volcado de cuotas y otros tipos de oportunidades, reduciendo el payload inicial de megabytes a kilobytes
  2. Envía pings cada 25 segundos — El servidor envía heartbeats cada 30s, pero los pings explícitos previenen los timeouts de proxy/firewall
  3. Usa filtros — Pasa los parámetros sport, sportsbook, league, market y event_id para acotar los datos dentro de tus canales suscritos
  4. Define umbrales — Usa min_ev y min_profit para filtrar las oportunidades de bajo valor en el servidor, reduciendo el ruido
  5. Actualiza vía subscribe — Cambia canales, filtros y umbrales sin reconectar
  6. Maneja los códigos de cierre — 4001 significa clave incorrecta o desplazamiento por una sesión más reciente (distínguelos por el motivo de cierre), 4003 significa que los permisos cambiaron durante el stream (bajada de plan, clave revocada, complemento eliminado) — reconecta para reautorizar, tras comprobar que el plan aún lo permite
  7. Realiza seguimiento de los recibos del servidor — Confirma snapshot:complete.last_seq tras un resume exitoso, o el piso connected.global_seq pendiente solo después de que su snapshot fresco/completo termine; nunca derives from_seq de datos arbitrarios o frames de heartbeat
  8. Implementa la reconexión — A diferencia de SSE, WebSocket no se reconecta automáticamente. Usa retroceso exponencial (1s, 2s, 4s, …) con replay de from_seq para cortes breves
  9. Espera a snapshot:complete — Esto indica que se han enviado todos los datos iniciales. Oculta los estados de carga después de recibirlo
  10. Maneja odds:removed — Elimina las cuotas de tu estado local cuando recibas este mensaje para evitar mostrar datos obsoletos
  11. Cierra las conexiones no utilizadas — Cada clave permite 1 stream concurrente por defecto; una segunda conexión con la misma clave desplaza a la más antigua (cierre 4001)

RelacionadoPermalink for this section

Last updated on