Sesión de suscriptor
La app se identifica primero con la API key de la aplicación y después inicia sesión con el usuario y la contraseña de la suscripción. La respuesta incluye un token firmado que se envía en la cabecera Authorization.
Se genera una sola vez en Ajustes → API móvil y se comparte con la app. Todas las rutas de /api/v1 excepto /health y la raíz de descubrimiento requieren esta cabecera.
Si aún no existe una clave, la API responde 503 api_key_not_configured. Si la clave es incorrecta, responde 401 invalid_api_key.
/auth/login
Requiere API key
Autentica una suscripción y emite el token de acceso.
| Campo | Tipo | Descripción |
|---|---|---|
| username | string | Usuario de la suscripción registrado en el panel. |
| password | string | Contraseña de la suscripción. |
POST https://panel.tuscarrerastv.lat/api/v1/auth/login
X-API-Key: TU_API_KEY
Content-Type: application/json
{
"username": "cliente@rtn.tv",
"password": "clave-del-cliente"
}
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 86400,
"subscription": {
"id": 12,
"name": "María López",
"username": "cliente@rtn.tv",
"due_date": "2026-12-01",
"status": "active",
"status_label": "Activa",
"days_left": 21
}
}
Usar el token
Incluye la API key y el token en todas las peticiones protegidas:
X-API-Key: <api_key>
Authorization: Bearer <access_token>
- · El token vence a las 24 horas; vuelve a iniciar sesión cuando la API responda 401.
- · Cambiar la contraseña de la suscripción invalida los tokens anteriores.
- · Si la suscripción vence, el token deja de dar acceso aunque aún no haya expirado.
- · Tras 5 intentos fallidos desde la misma IP y usuario, el login responde 429 durante 15 minutos.
/auth/me
API key + Bearer
Devuelve la suscripción asociada al token, útil para validar la sesión al abrir la app.
/settings
Requiere API key
Branding para la app: nombre del panel, enlaces absolutos del logotipo y del fondo de acceso, y los tres colores configurados. No requiere token de suscripción.
{
"name": "TusCarrerasTV",
"logo_url": "https://panel.tuscarrerastv.lat/static/uploads/logo_abc123.png",
"login_background_url": null,
"primary_color": "#10b981",
"secondary_color": "#0a111c",
"tertiary_color": "#34d399"
}
Los enlaces son null cuando el administrador no ha subido el recurso.
/app/version
Requiere API key
Devuelve la última versión del APK publicada en Ajustes y su URL absoluta de descarga. No requiere token de suscripción.
{
"version": "1.4.2",
"url": "https://panel.tuscarrerastv.lat/static/uploads/app_abc123.apk",
"available": true
}
Compara version con la versión instalada en el dispositivo. Si available es false, no hay APK publicado: version y url llegan en null.
Guarda el token en almacenamiento seguro del dispositivo. No lo incluyas en URLs ni lo compartas entre usuarios. El endpoint /health y la raíz de descubrimiento son públicos; el resto exige API key.
Canales y streams en vivo
Todos estos endpoints requieren X-API-Key y Authorization: Bearer <access_token>. Los datos provienen de RTN y el listado se cachea 90 segundos; usa ?refresh=1 para forzar una actualización.
/channels
Lista todos los canales con su estado, categoría y metadatos, más un agrupado por categoría.
| Parámetro | Tipo | Descripción |
|---|---|---|
| refresh | boolean | Si es 1, ignora la caché y consulta RTN. |
{
"date": "2026-11-10",
"count": 2,
"counts": { "on_air": 1, "upcoming": 1 },
"categories": [
{
"name": "Thoroughbred (United States)",
"order": 10,
"counts": { "total": 2, "on_air": 1, "upcoming": 1 },
"channels": [ /* canales de la categoría */ ]
}
],
"channels": [
{
"key": "abc123",
"name": "Aqueduct",
"category": "Thoroughbred (United States)",
"status": "on_air",
"stream": "aqueduct",
"racedate": "20261110",
"onAirTime": "12:50",
"duration": "8:00",
"channelNo": 1
}
]
}
/channels/<key>
Devuelve un canal por su clave. Responde 404 not_found si no existe.
/catalog
Catálogo completo de pistas disponibles (incluye las que no están en vivo), agrupado por categoría.
/stream/<key>
Resuelve el token Millicast y la URL del iframe para reproducir un canal.
| Parámetro | Tipo | Descripción |
|---|---|---|
| force | boolean | Fuerza la renovación del token aunque siga vigente. |
{
"key": "abc123",
"name": "Aqueduct",
"stream": "aqueduct",
"accountId": "ACCOUNT_ID",
"token": "eyJhbGciOi...",
"tokenExpires": 1762803600,
"expiresIn": 1800,
"millicastUrl": "millicast://ACCOUNT_ID/aqueduct",
"iframe": "https://stream.robertsstream.com/streamlive.php?...",
"apiRefresh": "/api/v1/stream/abc123"
}
El token de RTN vence; llama de nuevo al endpoint cuando expiresIn llegue a 0. La API reutiliza el token en caché mientras le queden más de 150 segundos de vigencia.
/playlist
Devuelve en una sola respuesta todos los canales actualmente en vivo con su token y URL de iframe. Ideal para una pantalla de inicio tipo parrilla.
Errores
Todas las respuestas de error son JSON con un campo error y, cuando aplica, code y fields.
| Código | Significado | Acción sugerida |
|---|---|---|
| 400 | Solicitud inválida o JSON mal formado | Revisa el cuerpo enviado. |
| 401 | API key inválida, credenciales incorrectas o token ausente/inválido | Revisa X-API-Key e inicia sesión de nuevo si el token caducó. |
| 403 | Suscripción vencida | Renueva la suscripción desde el panel. |
| 404 | Canal no encontrado | Verifica la clave del canal. |
| 422 | Validación de campos del login | Lee fields y corrige. |
| 429 | Demasiados intentos de login | Espera el tiempo indicado en Retry-After. |
| 502 | Error de RTN o del stream | Reintenta; si persiste, revisa la cuenta RTN. |
| 503 | API key no configurada en el panel | Genera una en Ajustes → API móvil. |
{
"error": "Usuario o contraseña incorrectos.",
"code": "invalid_credentials"
}
Ejemplos de uso
Login y canales con cURL
# 0. Branding de la app (solo API key)
curl -s "https://panel.tuscarrerastv.lat/api/v1/settings" \
-H "X-API-Key: TU_API_KEY"
# 1. Obtén el token
curl -s -X POST "https://panel.tuscarrerastv.lat/api/v1/auth/login" \
-H "X-API-Key: TU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username":"cliente@rtn.tv","password":"clave-del-cliente"}'
# 2. Usa access_token en las peticiones protegidas
curl -s "https://panel.tuscarrerastv.lat/api/v1/channels" \
-H "X-API-Key: TU_API_KEY" \
-H "Authorization: Bearer TU_ACCESS_TOKEN"
JavaScript (fetch)
const API = "https://panel.tuscarrerastv.lat/api/v1";
const API_KEY = "TU_API_KEY";
async function getBranding() {
const response = await fetch(`${API}/settings`, {
headers: { "X-API-Key": API_KEY, Accept: "application/json" },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function login(username, password) {
const response = await fetch(`${API}/auth/login`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
return data.access_token;
}
async function getLiveChannels(token) {
const response = await fetch(`${API}/channels`, {
headers: {
"X-API-Key": API_KEY,
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
if (!response.ok) {
const problem = await response.json().catch(() => ({}));
throw new Error(problem.error || `HTTP ${response.status}`);
}
const data = await response.json();
return data.channels.filter((channel) => channel.status === "on_air");
}
async function getStream(token, key) {
const response = await fetch(`${API}/stream/${encodeURIComponent(key)}`, {
headers: { "X-API-Key": API_KEY, Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error("No se pudo resolver el stream");
return response.json();
}
Integración en Android y Smart TV
Identificación: incluye la API key de la aplicación en X-API-Key en cada petición. Es una clave de app, no de usuario: se genera en Ajustes → API móvil y se comparte con la build mediante configuración segura.
Sesión: pide usuario y contraseña al suscriptor, llama a /auth/login con la API key y guarda el token en EncryptedSharedPreferences o DataStore. Al abrir la app, valida con /auth/me; si responde 401, vuelve al login.
Reproducción: RTN entrega un token Millicast y una URL de iframe. El campo iframe puede cargarse en un WebView (Android/Android TV) con JavaScript y autoplay habilitados. El esquema millicast:// no lo reproduce ExoPlayer/Media3 de forma nativa.
Renovación: guarda tokenExpires del stream y vuelve a pedir /stream/<key> unos minutos antes de que venza. No compartas el token entre dispositivos ni lo guardes en texto plano.
Seguridad: en producción usa HTTPS. La API key protege la app a nivel de aplicación; el acceso a canales depende además de la suscripción y puede revocarse venciéndola, cambiando su contraseña o regenerando la API key.
Kotlin + OkHttp (esqueleto)
private const val API_KEY = "TU_API_KEY"
private val client = OkHttpClient()
fun getBranding(): String {
val request = Request.Builder()
.url("https://panel.tuscarrerastv.lat/api/v1/settings")
.addHeader("X-API-Key", API_KEY)
.addHeader("Accept", "application/json")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) error("HTTP ${response.code}")
return response.body?.string().orEmpty()
}
}
fun login(username: String, password: String): String {
val body = """{"username":"$username","password":"$password"}"""
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://panel.tuscarrerastv.lat/api/v1/auth/login")
.addHeader("X-API-Key", API_KEY)
.post(body)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) error("HTTP ${response.code}")
val json = JSONObject(response.body?.string().orEmpty())
return json.getString("access_token")
}
}
fun getChannels(token: String): String {
val request = Request.Builder()
.url("https://panel.tuscarrerastv.lat/api/v1/channels")
.addHeader("X-API-Key", API_KEY)
.addHeader("Authorization", "Bearer $token")
.addHeader("Accept", "application/json")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) error("HTTP ${response.code}")
return response.body?.string().orEmpty()
}
}
El endpoint /health es público y no requiere token. Úsalo para monitoreo y para verificar conectividad desde la app antes de mostrar el login.
Tabla de endpoints
Todas las rutas son relativas a https://panel.tuscarrerastv.lat/api/v1.
| Método | Ruta | Autenticación | Descripción |
|---|---|---|---|
| GET | /health | Pública | Estado del servicio. |
| GET | /settings | API key | Nombre, logo, fondo de login y colores para la app. |
| GET | /app/version | API key | Última versión del APK y URL de descarga. |
| POST | /auth/login | API key | Login con credenciales de suscripción. |
| GET | /auth/me | API key + Bearer | Suscripción asociada al token. |
| GET | /channels | API key + Bearer | Canales y agrupación por categoría. |
| GET | /channels/{key} | API key + Bearer | Detalle de un canal. |
| GET | /catalog | API key + Bearer | Catálogo completo de pistas. |
| GET | /stream/{key} | API key + Bearer | Token e iframe de reproducción. |
| GET | /playlist | API key + Bearer | Todos los streams en vivo. |
La API no expone listado, creación, edición ni eliminación de suscripciones. Esas operaciones se realizan exclusivamente desde el panel administrativo.