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?

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ón

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 consulta

ParámetroTipoPor defectoDescripción
api_keystringObligatorio. 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_oddsnumberFiltrar cuotas por valor mínimo de cuota americana (p. ej., -200)
max_oddsnumberFiltrar cuotas por valor máximo de cuota americana (p. ej., 500)
statestringCó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.
resumebooleanfalseOmite el snapshot inicial de cuotas en la reconexión (asume que el cliente conserva el estado anterior)
from_seqintegerReproduce los eventos perdidos desde este número de secuencia global. Úsalo con resume para una reconexión sin huecos. 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ón

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 encontrada |← 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 mensajes

Cliente → Servidor

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

Servidor → Cliente

connected

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

{ "type": "connected", "seq": 1, "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 }, "timestamp": "2026-02-08T18:47:17.559Z" }
CampoTipoDescripción
seqintegerNúmero de secuencia de mensajes por conexión (se incrementa con cada mensaje)
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_seqintegerNúmero actual de secuencia de evento global. Almacénalo para la reconexión con replay.
books.maxintegerSportsbooks máximos permitidos para tu plan (-1 = ilimitado)
books.allowedstring[] | nullSportsbooks específicos permitidos, o null para todos

subscribed

Confirma tus canales y filtros activos.

{ "type": "subscribed", "seq": 2, "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 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", "seq": 3, "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).

initial

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

{ "type": "initial", "seq": 4, "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 (hasta 1000 cuotas cada uno). Si no necesitas las cuotas brutas, omite el canal odds para saltarlas por completo.

snapshot:complete

Indica que se han enviado todos los snapshots iniciales (oportunidades + cuotas). Es seguro ocultar los estados de carga tras recibir este mensaje.

{ "type": "snapshot:complete", "seq": 10, "books": ["draftkings", "fanduel", "pinnacle"], "resumed": false, "progressive": true, "timestamp": "2026-02-08T18:47:18.000Z" }
CampoTipoDescripción
booksstring[]Lista de sportsbooks incluidos en el snapshot inicial
resumedbooleantrue si se trataba de una conexión de reanudación (no se reenvían las cuotas)
progressivebooleantrue si las cuotas se entregaron de forma progresiva (fragmentadas por book)

odds:update

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:removed

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:detected

Nueva oportunidad +EV encontrada. Solo plan Pro o superior.

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

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:detected

Nueva oportunidad de arbitraje encontrada. 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:expired

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:detected

Nueva oportunidad de middle encontrada. 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:expired

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:detected

Nueva oportunidad de low-hold encontrada. 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:expired

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

heartbeat

Keep-alive enviado cada 30 segundos.

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

pong

Respuesta a un ping del cliente.

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

error

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", "seq": 152, "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 cierre

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
4001Fallo de autenticaciónComprueba tu API key
4003Sin acceso al streamingAñade el complemento WebSocket (99 $/mes) o actualiza a Enterprise
4029Límite de streams superadoCierra las conexiones no utilizadas (por defecto: 1 por clave; gana la conexión más reciente)

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 secuencia

Cada mensaje del servidor incluye un campo seq — un entero por conexión que se incrementa con cada mensaje. El mensaje connected también incluye global_seq, un contador de eventos a nivel de servidor.

Úsalos para:

  • Ordenación — Verifica que los mensajes llegan en orden comprobando que seq aumenta de forma monótona
  • Detección de huecos — Un hueco en seq indica que se perdió un mensaje (p. ej. por backpressure)
  • Replay de reconexión — Pasa global_seq como from_seq al reconectar para reproducir los eventos perdidos

Almacena global_seq del mensaje connected y haz seguimiento de seq desde cada mensaje posterior. Al reconectar, pasa la última secuencia vista como from_seq para recibir los eventos perdidos.

Reconexión con Replay

El servidor mantiene un buffer de replay de 2 minutos (hasta 2000 eventos). Para desconexiones breves, puedes reconectar sin perder datos:

wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&resume=true&from_seq=12900
ParámetroEfecto
resume=trueOmite el snapshot completo de cuotas (asume que el cliente conserva el estado anterior)
from_seq=NReproduce todos los eventos desde la secuencia global N

Los mensajes reproducidos incluyen "replay": true y "global_seq": N para que puedas distinguirlos de los eventos en directo.

let lastGlobalSeq = 0; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'connected') { lastGlobalSeq = msg.global_seq; } if (msg.global_seq) { lastGlobalSeq = msg.global_seq; } if (msg.replay) { console.log('Replayed event:', msg.type); } }; // On reconnect: function reconnect() { const params = new URLSearchParams({ api_key: 'YOUR_KEY', channels: 'ev,odds', resume: 'true', from_seq: lastGlobalSeq.toString() }); ws = new WebSocket(`wss://ws.sharpapi.io?${params}`); }

El buffer de replay conserva los eventos durante 2 minutos (máx. 2000 eventos). Si has estado desconectado más tiempo, omite resume y from_seq para recibir un snapshot completo en su lugar.

Ejemplos de código

// 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 concurrentes

Cada conexión WebSocket abierta cuenta como un stream contra tu límite.

PlanMáx. streams concurrentes
Complemento WebSocket (99 $/mes)10
EnterprisePersonalizado

Superar tu límite de streams cierra la conexión con el código 4029. Cierra las conexiones no utilizadas antes de abrir nuevas.

Buenas prácticas

  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 cierre4001 significa clave incorrecta, 4003 significa sin acceso al streaming, 4029 significa demasiadas conexiones
  7. Realiza seguimiento de los números de secuencia — Almacena global_seq para el replay en la reconexión. Usa resume=true&from_seq=N para una recuperación sin huecos
  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)

Relacionado

Last updated on