Estado de Juego en Vivo
Estado de juego en vivo agregado — marcadores, periodos, relojes, posesión y datos situacionales específicos de cada deporte — fusionado entre sportsbooks en una única vista autorizada por evento. Una fila por encuentro en vivo, seleccionada por consenso entre los libros que lo cubren. Un encuentro que acaba de terminar también permanece aquí durante una ventana breve como fila terminal — consulta Eventos finalizados.
GET /api/v1/gamestate
GET /api/v1/gamestate/{sport}Autenticación
Requiere API key. Disponible con el complemento Game State ($79/mes) o el
plan Enterprise. Las claves sin ninguno de los dos reciben 403 tier_restricted
con addon: "game_state" en el cuerpo del error. Añade el complemento desde la
página de facturación en cualquier plan de pago
(Hobby / Pro / Sharp); las claves Enterprise lo tienen incluido.
El estado de juego en vivo también se transmite a través del canal gamestate en
SSE y WebSocket
— consulta Streaming más abajo. (El acceso al streaming requiere el
complemento WebSocket o Enterprise, además del requisito de Game State
indicado anteriormente.)
Parámetros de Ruta
| Parámetro | Tipo | Descripción |
|---|---|---|
sport | string | (opcional) Un único deporte a devolver. Coincide con un deporte conocido del Atlas (soccer, tennis, esports, basketball, table_tennis, baseball, hockey, cricket, volleyball, handball, football, olympics, darts, boxing, rugby_league, mma, aussie_rules, snooker, rugby_union, golf, floorball, water_polo, lacrosse, futsal, other). Sin distinción entre mayúsculas y minúsculas. Un deporte desconocido devuelve un objeto data vacío — y también un deporte admitido que no tenga nada en directo en este momento; consulta la nota siguiente. |
Un objeto data vacío no significa que el deporte no esté admitido. El game state
se combina por deporte en una clave gamestate:{sport} con un TTL de 60 segundos
(GAMESTATE_TTL, deliberadamente el doble de la ventana livestate de 30 segundos);
si no hay nada en directo la clave expira y la respuesta es {"data":{}}, idéntica a
la de un deporte no cubierto. Varios de los deportes anteriores son estacionales.
Usa GET /api/v1/sports para ver event_count y live_count por deporte.
other es el bucket de reserva para deportes que Atlas no ha mapeado.
Sin el parámetro de ruta, devuelve todos los deportes con eventos en vivo.
Filtros de consulta
Ambos se aplican a /api/v1/gamestate y /api/v1/gamestate/{sport}. Los
valores no distinguen mayúsculas y se recortan. Si omites ambos, obtienes el
conjunto por defecto: todo lo que está en vivo, más las filas finalizadas que
siguen dentro de la ventana de arrastre.
| Parámetro | Aceptado | Descripción |
|---|---|---|
status | final | Devuelve únicamente las filas finalizadas arrastradas — el reflejo de /events?status=final. Cualquier otro valor es un 400 validation_error (ver abajo). No existe el valor live: las filas en vivo son el conjunto por defecto, y ?is_live=true las pide por separado. |
is_live | true, false | true devuelve solo filas en vivo, lo que excluye todas las filas finalizadas. false devuelve solo filas no en vivo, que hoy son las filas finalizadas arrastradas. Cualquier otro valor — incluidos 1 y 0 — es un 400 validation_error. |
Un valor no reconocido se rechaza, no se ignora. Tanto ?status=zzz como
?is_live=1 devuelven 400 con un cuerpo validation_error que nombra el
conjunto aceptado, en lugar de devolver en silencio las filas sin filtrar o
las del lado equivocado.
{
"error": {
"code": "validation_error",
"message": "is_live must be one of: true, false",
"details": { "parameter": "is_live", "accepted": ["true", "false"] }
}
}Antes ?is_live=1 se leía como false y devolvía exactamente las filas que
quien llamaba intentaba excluir; ahora es un 400. Si envías 1 / 0,
cambia a true / false.
Sobre de Respuesta
{
"data": {
"<sport>": {
"<event_id>": { ...event state... }
}
},
"updated_at": "2026-04-23T23:55:01.234Z"
}Cada estado de evento se indexa por su event_id canónico dentro del
contenedor de su deporte. updated_at es la hora del servidor en la que se
construyó esta respuesta — úsala para evaluar la frescura si haces polling.
Campos del Estado del Evento
Todos los campos son opcionales excepto donde se indique. La presencia varía
según el deporte — los eventos de tenis incluyen sets_home / server; los eventos
de hockey incluyen power_play; los eventos de fútbol incluyen corners_* /
yellow_cards_*; etc.
Siempre presentes
| Campo | Tipo | Notas |
|---|---|---|
home_team | string | Nombre normalizado del equipo local |
away_team | string | Nombre normalizado del equipo visitante |
sport | string | Contenedor del deporte (baseball, soccer, …) |
league | string | Identificador de liga del Atlas (p. ej. mlb, england_-_premier_league) |
home_score | integer | Puntuación actual del local en la unidad natural del deporte (carreras, puntos, goles, sets — el tenis usa la suma de puntos por set) |
away_score | integer | Puntuación actual del visitante |
is_live | boolean | true en un evento en vivo. false en una fila finalizada arrastrada — consulta Eventos finalizados |
primary_book | string | El libro del que se seleccionaron las puntuaciones fusionadas. Respeta el consenso — los libros de mayor calidad ganan los empates |
book_count | integer | Número de sportsbooks que aportaron estado en vivo para este evento en el momento de la fusión |
«Siempre presentes» significa siempre presentes en una fila en vivo. Una
fila finalizada arrastrada se construye a partir del registro de finalización,
no de una fusión en vivo, así que no lleva ni primary_book ni book_count,
ni game_period / game_clock. Sus campos de puntuación también dependen del
deporte: los deportes por sets llevan sets_home / sets_away en lugar de
home_score / away_score. Comprueba status === "final" antes de leer
cualquiera de estos campos. La forma terminal completa está en
Eventos finalizados.
Temporales (la mayoría de deportes de equipo)
| Campo | Tipo | Ejemplo |
|---|---|---|
game_period | string | "T5" (parte alta de la 5.ª entrada de MLB), "Q3" (Q3 de NBA), "2H" (2.ª parte de fútbol), "P2" (NHL), "S2" (2.º set de tenis), "FT" (tiempo completo) |
game_clock | string | "49:06" para deportes con cronómetro ascendente (fútbol), "5:42" para deportes con cuenta atrás (baloncesto, hockey) |
Situacionales
| Campo | Tipo | Notas |
|---|---|---|
possession | "home" | "away" | Qué equipo tiene la posesión (deportes de equipo) o está sirviendo (tenis) |
server | "home" | "away" | Solo tenis — sacador actual |
last_play | string | Descripción de la última jugada específica del deporte cuando esté disponible |
is_timeout | boolean | true durante un tiempo muerto |
Específicos por deporte
| Campo | Deporte | Tipo |
|---|---|---|
fouls_home, fouls_away | baloncesto, fútbol | integer |
corners_home, corners_away | fútbol | integer |
yellow_cards_home, yellow_cards_away | fútbol | integer |
red_cards_home, red_cards_away | fútbol | integer |
power_play | hockey | "home" | "away" |
sets_home, sets_away | tenis | integer (sets ganados) |
hits_home, hits_away | béisbol | integer |
home_pitcher, away_pitcher | béisbol | string — nombre visible del pitcher inicial |
wickets_home, wickets_away | cricket | integer |
overs | cricket | float |
batting_team | cricket | "home" | "away" |
Frescura
| Campo | Tipo | Significado |
|---|---|---|
stale | true (omitido en caso contrario) | El TTL de la clave de livestate del primary_book cayó por debajo del umbral de frescura del agregador (~10s) en el momento de la fusión. Las puntuaciones son los últimos valores conocidos; el libro ha quedado en silencio. Considera que las puntuaciones y el periodo del evento podrían ir retrasados respecto al estado real del partido. |
aggregator_stale | true (omitido en caso contrario) | El propio agregador de SharpAPI no ha escrito un nuevo shard gamestate:{sport} para este deporte en más de ~30s. Señal más amplia que stale — indica un problema en el pipeline, no de un único libro. Si lo observas de forma persistente, contacta con soporte. |
Ausencia = fresco. Tanto stale como aggregator_stale solo se
escriben cuando son verdaderos, manteniendo el payload compacto. Si no los
ves en un evento, los datos se consideran frescos.
Eventos finalizados
Antes, un encuentro desaparecía de /gamestate en el momento en que terminaba
— el marcador final solo era visible para el cliente que casualmente hiciera
polling durante el último ciclo en vivo. Ahora los eventos finalizados
permanecen en este endpoint 15 minutos después de terminar como fila
terminal, de modo que cualquier cliente con un intervalo razonable ve el
resultado final al menos una vez.
La ventana es del lado servidor y actualmente es de 15 minutos
(GAMESTATE_TERMINAL_CARRY_SECONDS, por defecto 900). Cuando expira, la fila
desaparece definitivamente; el resultado ya cerrado sigue disponible en
/api/v1/events?status=final, que es de donde
procede la fila terminal.
Cómo es una fila terminal
{
"home_team": "Skelleftea AIK",
"away_team": "Geneva Servette HC",
"sport": "hockey",
"league": "champions_hockey_league",
"home_score": 2,
"away_score": 1,
"score_type": "points",
"winner": "home",
"status": "final",
"completed_at": "2026-09-10T18:54:03Z",
"is_live": false,
"stale": false
}| Campo | Tipo | Notas |
|---|---|---|
status | "final" | Presente solo en una fila terminal. Las filas en vivo no llevan ninguna clave status, así que status === "final" es la comprobación fiable — no lo deduzcas de is_live. |
completed_at | string | ISO 8601 — cuándo detectó SharpAPI la finalización, no el pitido final oficial de la liga. La ventana de arrastre se mide desde esta marca de tiempo. |
score_type | "points" | "sets" | Qué campos llevan el resultado. "sets" para los deportes por sets (tennis, table_tennis, volleyball, badminton); "points" en todo lo demás. |
winner | "home" | "away" | "draw" | Opcional — se omite cuando el resultado no puede determinarse; ver abajo. "draw" solo se produce en deportes donde un marcador igualado es un resultado real (soccer, hockey, football, rugby_union, rugby_league). |
is_live | false | Siempre false de forma explícita, para que cualquier renderizado condicionado por is_live trate la fila como no en vivo. |
stale | false | Siempre false de forma explícita. Es el único sitio donde stale aparece sin ser verdadero — la regla «ausencia = fresco» de arriba vale solo para filas en vivo. |
Los marcadores finales usan los nombres de campo en vivo, según el deporte.
Un deporte por puntos pone su cómputo en home_score / away_score. Un deporte
por sets pone su recuento de sets en sets_home / sets_away y no lleva
home_score / away_score — un final de tenis es 2-1 en sets, y ponerlo en
home_score chocaría con los puntos de juego de la fila en vivo:
{
"home_team": "Mikhail Biserov",
"away_team": "Vasily Yugov",
"sport": "table_tennis",
"league": "pro_league",
"sets_home": 2,
"sets_away": 1,
"score_type": "sets",
"winner": "home",
"status": "final",
"completed_at": "2026-09-10T18:55:25Z",
"is_live": false,
"stale": false
}winner puede faltar, y eso significa algo. Se omite cuando el resultado
registrado no cierra el encuentro:
- un marcador igualado en un deporte que no admite empate (baloncesto, balonmano, esports, béisbol…) — el partido fue a la prórroga, se suspendió, o el feed se cortó antes de la resolución;
- un recuento de sets sub-terminal en un deporte por sets (quien lidera con menos de dos sets no puede haber ganado ningún formato);
- un snapshot cuyas puntuaciones nunca llegaron, que se registra como
0-0. Por eso un0-0sinwinnerno es lo mismo que un empate a cero real — el real lleva"winner": "draw".
Trata un winner ausente como «sin resolver», no como un empate. Si estás
calificando o liquidando algo, salta estas filas.
Qué campos no lleva una fila terminal
Una fila terminal se proyecta desde el registro de finalización, no desde una
fusión en vivo entre libros, así que estos campos en vivo simplemente no están:
primary_book, book_count, game_period, game_clock y todos los campos
situacionales (possession, corners_*, power_play, in_play, la familia de
probabilidad de victoria de tenis, etc.). Léelos solo después de comprobar que
la fila no es status: "final".
Filtrar eventos finalizados
| Lo que quieres | Petición |
|---|---|
| Solo filas en vivo (comportamiento previo al arrastre) | ?is_live=true |
| Solo filas finalizadas | ?status=final |
| Ambas (por defecto) | ningún parámetro |
# Solo los finales de los últimos 15 minutos, un deporte
curl "https://api.sharpapi.io/api/v1/gamestate/soccer?status=final" \
-H "X-API-Key: YOUR_API_KEY"
# Exactamente lo que obtenías antes de que existiera el arrastre
curl "https://api.sharpapi.io/api/v1/gamestate?is_live=true" \
-H "X-API-Key: YOUR_API_KEY"Si un evento finalizado sigue en vivo en algún sitio, gana la fila en
vivo. Un event_id que aparece tanto en el conjunto en vivo como en el
arrastrado se devuelve una sola vez, como fila en vivo. Un libro que
siga cotizando el partido lo mantiene en vivo hasta que todos lo hayan
soltado.
Ejemplos de Solicitudes
cURL
# Todos los eventos en vivo de cada deporte
curl "https://api.sharpapi.io/api/v1/gamestate" \
-H "X-API-Key: YOUR_API_KEY"
# Un solo deporte
curl "https://api.sharpapi.io/api/v1/gamestate/soccer" \
-H "X-API-Key: YOUR_API_KEY"Ejemplo de Respuesta
{
"data": {
"soccer": {
"argentina_-_primera_division_bocajuniors_defensayjusticia_2026-04-23": {
"home_team": "Defensa y Justicia",
"away_team": "Boca Juniors",
"sport": "soccer",
"league": "argentina_-_primera_division",
"home_score": 0,
"away_score": 1,
"game_period": "2H",
"game_clock": "49:06",
"is_live": true,
"possession": "away",
"corners_home": 1,
"corners_away": 0,
"fouls_home": 0,
"fouls_away": 0,
"yellow_cards_home": 0,
"yellow_cards_away": 0,
"red_cards_home": 0,
"red_cards_away": 0,
"primary_book": "draftkings",
"book_count": 6
},
"chile_-_primera_division_concepcion_palestino_2026-04-24": {
"home_team": "Palestino",
"away_team": "Concepción",
"sport": "soccer",
"league": "chile_-_primera_division",
"home_score": 0,
"away_score": 0,
"is_live": true,
"primary_book": "unibet",
"book_count": 1,
"stale": true
},
"montenegro_-_prva_liga_bokelj_otrantolympiculcinj_2026-04-23": {
"home_team": "Bokelj",
"away_team": "FK Otrant-Olympic Ulcinj",
"sport": "soccer",
"league": "montenegro_-_prva_liga",
"home_score": 2,
"away_score": 1,
"score_type": "points",
"winner": "home",
"status": "final",
"completed_at": "2026-04-23T23:48:55Z",
"is_live": false,
"stale": false
}
}
},
"updated_at": "2026-04-23T23:55:01.234Z"
}La tercera fila es una fila terminal: el partido terminó
unos seis minutos antes de que se construyera esta respuesta, así que se
arrastra con status: "final" y su marcador final, y no lleva primary_book,
book_count, game_period ni game_clock.
Modelo de Fusión Entre Libros
Los campos de cada evento se fusionan a partir del snapshot de livestate de cada sportsbook mediante un algoritmo de tres clases:
- Clase A — Puntuaciones se eligen por consenso: entre los libros con el rango de periodo más avanzado, gana la puntuación total más alta con ≥2 respaldos. Una puntuación atípica reportada por un único libro (común en los primeros segundos de un cambio de marcador, o por un adaptador con mal comportamiento) se rechaza.
- Clase B — Campos temporales (
game_period,game_clock) se seleccionan según la dirección del reloj específica del deporte — cuenta atrás frente a cuenta ascendente — y el rango del periodo. - Clase C — Campos situacionales (
possession,corners_*, etc.) se rellenan por prioridad a partir de una clasificación fija de libros.
primary_book indica de qué libro provienen las puntuaciones ganadoras.
book_count es el número total de libros que aportaron cualquier estado
para el evento en el momento de la fusión.
Streaming
Cada actualización en vivo que el endpoint REST mostraría en el siguiente
poll también dispara un evento gamestate:update en los canales de streaming:
- SSE:
GET /api/v1/stream/gamestate - WebSocket: suscríbete a
{channels: ["gamestate"]}enwss://ws.sharpapi.io/ws
Los clientes de streaming reciben un gamestate:snapshot inicial con el
estado actual completo (una lista plana de filas de eventos), después
eventos gamestate:update que llevan las filas modificadas conforme se
fusionan, y eventos gamestate:removed cuando los eventos salen del
conjunto en vivo.
Un evento finalizado se queda en el slate transmitido como su fila
status: "final" durante la ventana descrita en
Eventos finalizados, y se anuncia una vez con un
frame gamestate:final.
Avisos de evento finalizado
Cuando un evento termina, cada transporte envía un único frame
gamestate:final para él. Lleva la fila terminal del evento — la misma
forma que la fila REST, más su event_id.
WebSocket:
{"type":"gamestate:final","seq":48213,"global_seq":"48213","data":[{"event_id":"atp_jdoe_rroe_2026-07-03","home_team":"J. Doe","away_team":"R. Roe","sport":"tennis","league":"atp","sets_home":1,"sets_away":2,"score_type":"sets","winner":"away","status":"final","completed_at":"2026-07-03T19:42:10Z","is_live":false,"stale":false}]}SSE (el mismo envoltorio {"data": [...]} que gamestate:update):
event: gamestate:final
data: {"data":[{"event_id":"atp_jdoe_rroe_2026-07-03","home_team":"J. Doe","away_team":"R. Roe","sport":"tennis","league":"atp","sets_home":1,"sets_away":2,"score_type":"sets","winner":"away","status":"final","completed_at":"2026-07-03T19:42:10Z","is_live":false,"stale":false}]}- Mismo acceso y mismos filtros que
gamestate:update. Si recibesgamestate:update, recibesgamestate:final, y tus filtrossport/leaguese aplican. Sin acceso a game state no recibes ninguno de los dos. - WebSocket lo reproduce. Igual que
gamestate:updateygamestate:removed, queda en el buffer de replay y se reenvía en una reconexión confrom_seq. Los frames de gamestate por SSE no son reanudables. - Es un aviso, no la única entrega. La misma fila está también en el
slate (
gamestate:snapshotygamestate:update) durante toda la ventana.
“Una vez por evento” puede repetirse ocasionalmente — deduplica por
(event_id, completed_at). Puedes recibir gamestate:final más de una
vez para el mismo evento. event_id y completed_at son idénticos en
cada repetición: descarta un frame cuyo
par ya procesaste. Siempre es seguro, porque la fila sigue en el slate.
Rate Limits y Cuotas
Los rate limits siguen tu plan base, no el complemento — las claves Hobby
mantienen sus 120 req/min, Pro mantiene 300, Sharp mantiene 1.000, Enterprise
personalizado. Consulta Precios . /gamestate
cuenta igual que otras llamadas REST en la cuota por minuto.
Resolución de Problemas
- 403
tier_restrictedconaddon: "game_state"— tu clave no tiene el complemento Game State. Añádelo desde Facturación por $79/mes, o actualiza a Enterprise. - 400
validation_errorconparameter: "status"— el único valor aceptado de?status=esfinal. No existe el valorlive; las filas en vivo son el conjunto por defecto, o pídelas por separado con?is_live=true. - 400
validation_errorconparameter: "is_live"—?is_live=solo aceptatrue/false.1y0se rechazan; antes se leían comofalsey devolvían lo contrario de lo pedido. - Una fila con
status: "final"yis_live: false— no es un fallo. Es un evento finalizado dentro de su ventana de 15 minutos. Filtra con?is_live=truepara el comportamiento anterior. - Faltan
primary_book/book_count— estás leyendo una fila terminal. Compruebastatusantes de leer campos que solo existen en vivo. - Objeto
datavacío — no hay eventos en vivo en el ámbito. Esto es habitual entre eventos de calendarios deportivos más pequeños. Vuelve a hacer polling. - Eventos con
stale: true— el libro principal ha quedado en silencio. Las puntuaciones pueden ir con retraso; considera filtrarlos o mostrar un indicador en la interfaz. - Eventos con
aggregator_stale: true— el agregador de SharpAPI no ha refrescado ese deporte en >30s. Si es persistente, contacta con soporte; un breve aumento puede ocurrir durante los redespliegues. - El mismo partido aparece dos veces bajo
event_ids diferentes — limitación conocida en algunas ligas regionales donde los sportsbooks usan nomenclatura de liga inconsistente. Usa(home_team, away_team)como clave secundaria de deduplicación en el cliente hasta que se cierren los huecos de alias restantes.