Documentación · v2.0.0
API TUSSAM
API REST de código abierto para consultar paradas, líneas y tiempos de llegada de los autobuses de TUSSAM (Sevilla) en tiempo real. Actúa como proxy inteligente sobre la API interna de TUSSAM: normaliza los datos, cachea los tiempos, reintenta con criterio ante los errores del origen y expone CORS para poder llamarse desde cualquier cliente.
Toda la superficie de lectura es pública y no requiere autenticación. Solo los endpoints de sincronización (escritura) están protegidos por una API key. Las respuestas son JSON con coordenadas en grados decimales; el endpoint agregado admite además salida GeoJSON.
reddelineas.tussam.es de forma responsable (cache y rate limiting) y expone los datos con una interfaz estable.
URL base y formato
En el despliegue con Docker Compose incluido, la API escucha en http://localhost:8081. En producción, la URL base es la de tu propio despliegue. Todos los ejemplos de esta página usan el puerto local.
- Todas las respuestas son
application/json(salvoformato=geojson, que devuelve unFeatureCollection). - Las coordenadas se expresan en grados decimales (WGS84), no en el formato E6 del origen.
- Cada respuesta incluye cabeceras
X-RateLimit-*con la cuota restante. - Cabeceras de seguridad activas:
X-Content-Type-Options,X-Frame-Options,Referrer-PolicyyStrict-Transport-Securityen producción.
Autenticación
Los endpoints de lectura son públicos. Los de sincronización (POST /sync/*) exigen la cabecera X-API-Key con el valor de la variable de entorno de la clave de sync. La comparación usa hmac.compare_digest para no filtrar información por tiempo de respuesta.
curl -X POST http://localhost:8081/sync/all \
-H "X-API-Key: $MI_CLAVE_DE_SYNC"
El servicio se niega a arrancar la operación si la clave está sin definir o conserva el valor de ejemplo (responde 503). El flag ALLOW_UNAUTHENTICATED_SYNC permite desactivar la autenticación solo fuera de producción; con APP_ENV=production se rechaza siempre.
Rate limiting
Cada petición se contabiliza en dos cubos que se aplican de forma conjunta: uno por IP (techo anti-DDoS) y otro, más estricto, por dispositivo. Se responde 429 si cualquiera de los dos supera su límite. El identificador de dispositivo solo puede restringir más, nunca saltarse el límite por IP.
| Ámbito | Límite | Clave |
|---|---|---|
| Por IP | 300 req/min | IP del cliente (real tras proxy de confianza) |
| Por dispositivo | 60 req/min | Cabecera X-Device-ID (opcional) |
Un cliente que envíe un X-Device-ID estable (por ejemplo, un UUID generado al instalarse) obtiene su propia cuota de 60/min sin competir con el resto de usuarios de su misma IP. Cada respuesta trae:
X-RateLimit-Limit: límite aplicable.X-RateLimit-Remaining: peticiones restantes en la ventana.X-RateLimit-Reset: segundos hasta que se libere cuota.Retry-After(solo en 429): segundos que conviene esperar.
/health está exento del rate limiting.
Resiliencia y cache
El origen (reddelineas.tussam.es) devuelve 429 con facilidad y a veces cambia el formato de la respuesta. La API está construida para que el cliente nunca herede esos problemas:
- Cache fresco (TTL 60 s): los tiempos se sirven desde SQLite; el origen solo se consulta cuando expiran.
- Single-flight por parada: muchas peticiones simultáneas a la misma parada generan una sola llamada al origen.
- Reintentos con backoff: los 429 y 5xx se reintentan respetando el
Retry-Afterque envíe el origen. - Cache stale (hasta 10 min): si el origen cae, se sirve la última respuesta válida marcada con
stale: truey sucached_at.
Los endpoints señalizan siempre el estado del origen para que el cliente distinga «no vienen buses» de «no pudimos consultar»: mediante upstream_status en /paradas/{codigo}/tiempos y tiempos_status por parada en /cercanas.
Referencia de endpoints
/cercanas
Público
Endpoint principal. Devuelve las paradas más cercanas a una ubicación con sus tiempos de llegada, en una sola llamada. Es el recomendado para apps, webs e integraciones que parten de coordenadas.
Parámetros
| Parámetro | Tipo | Def. | Descripción |
|---|---|---|---|
lat obligatorio | float | — | Latitud del usuario (−90 a 90) |
lon obligatorio | float | — | Longitud del usuario (−180 a 180) |
radio | int | 300 | Radio de búsqueda en metros (50–2000) |
max_paradas | int | 3 | Máximo de paradas a devolver (1–10) |
bearing | float | null | Orientación del usuario en grados (0–360) |
bearing_tolerance | float | 60 | Tolerancia de orientación (0–180) |
tiempo_max | int | null | Solo buses que lleguen en ≤ X minutos |
lineas | string | null | Filtrar líneas separadas por comas (ej: 01,C4) |
sentido | int | null | Filtrar por sentido: 1 (ida) o 2 (vuelta) |
formato | string | json | json o geojson |
incluir_mapa | bool | false | Añade una URL de OpenStreetMap por parada |
Ejemplo
curl "http://localhost:8081/cercanas?lat=37.3896&lon=-5.9842&max_paradas=2"
{
"ubicacion": { "lat": 37.3896, "lon": -5.9842, "bearing": null },
"paradas": [
{
"codigo": "43",
"nombre": "Recaredo (Puerta Carmona)",
"latitud": 37.389663,
"longitud": -5.984265,
"distancia": 74,
"calle": "Calle Recaredo",
"numero": "5",
"codigo_postal": "41003",
"municipio": "Sevilla",
"direccion_completa": "Calle Recaredo 5",
"tiempos_status": "ok",
"tiempos": [
{ "linea": "01", "color": "#f54129", "tiempo_minutos": 2,
"destino": "POL. NORTE", "distancia_metros": 420, "sentido": 1 }
]
}
]
}
Señalización del estado del origen
Cada parada incluye tiempos_status para que el cliente interprete correctamente una lista de tiempos vacía:
| Valor | Significado |
|---|---|
ok | Consulta correcta. La lista vacía significa que no vienen buses. |
unavailable | El origen no respondió y no había cache que servir. |
error | Error inesperado al procesar la parada. |
Si los datos provienen de cache antigua, la parada añade stale: true y cached_at con el instante de la última consulta buena.
provincia, comunidad_autonoma y updated_at no se incluyen aquí; para obtenerlos usa GET /paradas/{codigo}.
/paradas
Público
Devuelve el catálogo completo de paradas (967 actualmente) con todos sus campos, incluida la dirección postal. Ordenadas por código.
{
"codigo": "889",
"nombre": "Recaredo (San Roque)",
"latitud": 37.391250,
"longitud": -5.984236,
"calle": "Calle Recaredo",
"numero": "5",
"codigo_postal": "41003",
"municipio": "Sevilla",
"provincia": "Sevilla",
"comunidad_autonoma": "Andalucía",
"direccion_completa": "Calle Recaredo 5",
"updated_at": "2026-02-16T20:41:18"
}
/paradas/cercanas
Público
Paradas cercanas sin tiempos de llegada. Más rápido que /cercanas; útil para pintar marcadores en un mapa. Devuelve todas las paradas dentro del radio (sin límite de número), cada una con distancia, bearing y bearing_diff.
| Parámetro | Tipo | Def. | Descripción |
|---|---|---|---|
lat obligatorio | float | — | Latitud (−90 a 90) |
lon obligatorio | float | — | Longitud (−180 a 180) |
radio | int | 500 | Radio en metros (50–2000). Nota: aquí el defecto es 500. |
bearing | float | null | Orientación del usuario (0–360) |
bearing_tolerance | float | 60 | Tolerancia en grados (0–180) |
/paradas/{codigo}
Público
Datos completos de una parada por su código. El código debe ser numérico (1–7 dígitos); un formato inválido se rechaza con 422 y una parada inexistente con 404.
curl http://localhost:8081/paradas/43
/paradas/{codigo}/tiempos
Público
Tiempos de llegada en tiempo real de una parada concreta. A diferencia de /cercanas, aquí la respuesta se ajusta al modelo TiemposParadaOut con metadatos explícitos de frescura y estado del origen.
{
"parada": "43",
"nombre": "Recaredo (Puerta Carmona)",
"latitud": 37.389663,
"longitud": -5.984265,
"tiempos": [
{ "linea": "01", "color": "#f54129", "tiempo_minutos": 2,
"destino": "POL. NORTE", "distancia_metros": 420, "sentido": 1 }
]
}
{
"parada": "43",
"nombre": "Recaredo (Puerta Carmona)",
"latitud": 37.389663,
"longitud": -5.984265,
"tiempos": [],
"upstream_status": "unavailable",
"upstream_detail": "TUSSAM API no disponible. Inténtalo en unos segundos."
}
Campos de cada tiempo
| Campo | Tipo | Descripción |
|---|---|---|
linea | string | Número de línea (ej: 01, C4) |
color | string | Color hex oficial de la línea |
tiempo_minutos | int | Minutos hasta la llegada. Puede ser negativo (bus ya en la parada) |
destino | string | Nombre del destino final |
distancia_metros | int / null | Distancia del bus a la parada |
sentido | int / null | 1 (ida), 2 (vuelta), o null si la línea pasa en ambos sentidos |
/paradas/{codigo}/lineas
Público
Lista de números de línea que pasan por una parada. Ejemplo de respuesta: ["01", "C4"].
/lineas
Público
Catálogo de líneas de TUSSAM con su color oficial, número, nombre y horarios de ida y vuelta.
{
"numero": "01",
"nombre": "Pol. Norte - H. Virgen del Rocío",
"color": "#f54129",
"sublinea": null,
"hora_inicio_ida": "06:00",
"hora_fin_ida": "23:30",
"hora_inicio_vuelta": "06:00",
"hora_fin_vuelta": "23:30",
"updated_at": "2026-02-16T20:41:18"
}
/lineas/{linea_numero}/paradas
Público
Recorrido de una línea: sus paradas ordenadas por sentido y orden. El número de línea es alfanumérico (1–8 caracteres); un formato inválido devuelve 422. Cada parada incluye los campos habituales más sentido y orden.
/health
Público
Estado de la API y de la base de datos para Docker, balanceadores y monitorización. No consulta el origen ni consume cuota de rate limiting.
{ "status": "ok", "db": "connected", "paradas_en_db": 967, "version": "2.0.0" }
status: "ok"— operativo con datos.status: "no_data"— la base de datos responde pero está vacía (falta el sync inicial).503— la base de datos no responde.
/sync/*
API key
Endpoints de sincronización que reconstruyen el catálogo desde el origen. Requieren X-API-Key. Solo se ejecuta una sincronización a la vez: si hay una en curso, el resto responde 409. El scheduler los ejecuta automáticamente una vez por semana.
| Endpoint | Acción |
|---|---|
POST /sync/all | Paradas + líneas + relaciones, en orden. |
POST /sync/paradas | Solo el catálogo de paradas. |
POST /sync/lineas | Solo el catálogo de líneas. |
POST /sync/paradas-lineas | Solo las relaciones parada-línea. |
POST /sync/direcciones | Geocodifica paradas sin dirección con Nominatim (~4 min). |
Códigos de error
| Código | Significado |
|---|---|
400 | Parámetro de negocio inválido (lat/lon/bearing/formato/sentido fuera de rango). |
403 | API key inválida o ausente en un endpoint de sync. |
404 | Parada inexistente (con formato de código válido). |
409 | Ya hay una sincronización en curso. |
422 | Validación de formato en el borde (código no numérico, radio o max_paradas fuera de rango). |
429 | Rate limit superado. Consulta Retry-After. |
500 | Error interno inesperado. |
503 | Base de datos no disponible, o configuración de sync insegura. |
Despliegue
La vía más rápida es la imagen publicada en el GitHub Container Registry (ghcr.io/686f6c61/api-tussam), que incluye una base de datos precargada: la API responde desde el primer arranque sin necesidad de sincronizar. La clave de sync solo hace falta para los endpoints de administración; expórtala en tu entorno (por ejemplo, generada con openssl rand -hex 24).
# Descarga la imagen del GitHub Container Registry
docker pull ghcr.io/686f6c61/api-tussam:main
# Arráncala (escucha en el puerto 8081; la clave solo hace falta para /sync/*)
export MI_CLAVE_DE_SYNC=$(openssl rand -hex 24)
docker run -d -p 8081:8080 \
-e SYNC_API_KEY=$MI_CLAVE_DE_SYNC \
ghcr.io/686f6c61/api-tussam:main
# Verifica el estado
curl http://localhost:8081/health
export MI_CLAVE_DE_SYNC=$(openssl rand -hex 24)
SYNC_API_KEY=$MI_CLAVE_DE_SYNC docker compose up -d
pip install -e .
export MI_CLAVE_DE_SYNC=$(openssl rand -hex 24)
SYNC_API_KEY=$MI_CLAVE_DE_SYNC uvicorn app.main:app \
--host 0.0.0.0 --port 8080 --proxy-headers
Variables de entorno
| Variable | Def. | Descripción |
|---|---|---|
APP_ENV | development | production desactiva /docs, activa HSTS y bloquea sync inseguro. |
SYNC_API_KEY | — | Clave para los endpoints de sincronización. Obligatoria para operarlos. |
ENABLE_DOCS | según entorno | Expone /docs, /redoc y /openapi.json. |
CORS_ORIGINS | * (dev) | Lista blanca de orígenes permitidos (separados por comas). |
ALLOWED_HOSTS | — | Hosts permitidos (Host header). localhost se añade siempre. |
TRUSTED_PROXY_IPS | — | IPs de proxy de las que se acepta X-Forwarded-For. |
TIEMPOS_CACHE_TTL_SECONDS | 60 | TTL del cache fresco de tiempos. |
TIEMPOS_STALE_TTL_SECONDS | 600 | Antigüedad máxima de la cache stale de respaldo. |
SYNC_MIN_COMPLETENESS_RATIO | 0.8 | Proporción mínima del catálogo que un sync debe recuperar para reemplazarlo. Evita mutilar la red al sincronizar en franjas de baja actividad. |
SYNC_ENABLED / SYNC_DAY / SYNC_HOUR / SYNC_MINUTE | true / sun / 11 / 0 | Programación del sync semanal (hora en UTC; mediodía por defecto, con la red en servicio). |