API de Tuneping v1
Una API JSON para saber lo que suena ahora mismo en las emisoras de radio por internet, y lo que pusieron antes. URL base https://api.tuneping.com/v1. Solo metadatos: el servicio lee la línea de título pública de cada stream y nunca toca el audio.
Inicio rápido
¿Quién está poniendo Madonna ahora mismo?
curl "https://api.tuneping.com/v1/now-playing/search?q=madonna&limit=3"
Lo que suena en una emisora que conoces por su id (los ids son UUID de emisora de Radio Browser):
curl "https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/now-playing"
Lo que puso una emisora en las últimas 24 horas (necesita una clave con historial, plan Developer o superior):
curl -H "Authorization: Bearer tp_live_…" \
"https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/history"
Todas las respuestas son JSON. Los errores son application/problem+json (consulta Errores). Los parámetros de consulta desconocidos se rechazan con 400 en lugar de ignorarse, así que las erratas salen a la luz enseguida.
Obtener una clave
Autoservicio en tuneping.com/signup: escribe una dirección de correo, abre el enlace recibido en menos de 60 minutos y la clave se muestra una vez. Esos mismos dos pasos son una API: POST /v1/signup con {"email": "…", "plan": "free|developer|pro"} responde 202 y envía el correo; POST /v1/signup/verify con el {"token": "…"} del enlace responde 201 con la clave. Free no necesita nada más. Developer y Pro continúan a Stripe Checkout desde el enlace de la respuesta; el plan del titular cambia en menos de un minuto tras el pago y todas las claves del titular lo siguen.
¿Perdiste una clave? Regístrate de nuevo con la misma dirección: la clave se rota y la anterior sigue funcionando durante 24 horas. Una clave live en Free y Developer, cinco en Pro, veinte en Business; además, una clave tp_test_ en todos los planes, emitida a petición hasta que esté disponible el portal.
Autenticación
Envía tu clave como token bearer o en X-Api-Key:
Authorization: Bearer tp_live_a1b2c3…
X-Api-Key: tp_live_a1b2c3…
Las claves empiezan por tp_live_ o tp_test_ seguido de 43 caracteres. Una solicitud sin clave se trata como anónima y se limita por dirección IP. Una clave mal formada, desconocida o revocada responde 401 de inmediato, incluso en endpoints que habrían funcionado de forma anónima. Los endpoints marcados más abajo con clave responden 401 unauthenticated si no se envía una.
Ámbitos. Las claves tienen todos los ámbitos (read, exports, webhooks, wishlists) salvo que se emitieran con una lista más restringida, por ejemplo de solo lectura; GET /v1/me muestra los ámbitos de una clave. Las listas de deseos y los webhooks necesitan los ámbitos wishlists y webhooks (una lista de deseos con entrega por webhook necesita ambos, y se comprueba antes que el plan). Una clave sin el ámbito responde 403 forbidden_scope con required_scope, y WWW-Authenticate lleva error="insufficient_scope".
Claves de prueba. Una clave tp_test_ lee los mismos datos en vivo con los límites de Free, y tiene sus propias listas de deseos, coincidencias y endpoints de webhook (livemode: false). Nunca ve ni modifica los de producción, así que un conjunto de pruebas puede hacer PUT de un conjunto vacío de listas de deseos sin tocar producción. Los webhooks en modo de prueba solo pueden apuntar a webhook.site.
Reintentos. POST, PATCH y DELETE en los endpoints que necesitan una clave o un token de socio aceptan una cabecera Idempotency-Key (un UUID por solicitud lógica); los endpoints de registro no la admiten. Un reintento con la misma clave de idempotencia y la misma solicitud vuelve a recibir la primera respuesta correcta, marcada con Idempotent-Replayed: true, en lugar de crear una segunda lista de deseos o un segundo endpoint. La clave de idempotencia se recuerda durante 24 horas; reutilizarla para una solicitud distinta da 422 idempotency_key_reused, y un reintento mientras la primera solicitud sigue en curso da 409 idempotency_in_progress. Los errores no se recuerdan, así que un reintento después de un error se vuelve a ejecutar. Una conexión cortada no cancela una solicitud que lleva la cabecera: la solicitud termina y, si la respuesta es correcta, se recuerda, de modo que tu reintento la recibe. Una solicitud que sigue en curso tras 20 segundos se cancela sin cambiar nada y responde 503 unavailable con Retry-After: 1; reinténtala con la misma clave de idempotencia.
Apps de socios (actualmente Tapelark) envían un token de derecho de uso firmado en lugar de una clave: Authorization: Bearer TPE1-…. El token se verifica sin conexión con las claves Ed25519 del socio y se resuelve en el plan de socio de la tabla de abajo; la API nunca sabe quién es el usuario final. Un token mal formado o desconocido responde 401 token_invalid, uno caducado 401 token_expired (renuévalo), un token para otra audiencia 401 token_audience, y un derecho de uso vencido o revocado 403 entitlement_revoked.
Límites de frecuencia y cuotas
| Plan | Frecuencia | Cuota | Ids por lote | Historial |
|---|---|---|---|---|
| Anónimo (por IP) | 2 req/s, ráfaga de 5 | 1.000 / día | 20 | ninguno |
| Free | 2 req/s, ráfaga de 5 | 1.000 / día | 20 | ninguno |
| Developer | 10 req/s, ráfaga de 20 | 100.000 / mes | 100 | 7 días |
| Pro | 50 req/s, ráfaga de 100 | 1.000.000 / mes | 100 | 12 meses |
| Business | a medida | a medida | 100 | 12 meses |
| Tapelark (token de derecho de uso, por licencia) | 5 req/s, ráfaga de 10 | 2.000 / día | 50 | 7 días |
El límite por segundo es un token bucket. Cuando está vacío recibes 429 rate_limited con Retry-After. Cuando la cuota se agota recibes 429 quota_exceeded con Retry-After fijado en los segundos que faltan para que se restablezca la ventana.
Las respuestas autenticadas que no se pueden almacenar en una caché compartida llevan los campos de límite de frecuencia del IETF:
RateLimit-Policy: "burst";q=10;w=1, "monthly";q=100000;w=2592000
RateLimit: "burst";r=7;t=1
Las respuestas que sí se pueden almacenar en una caché compartida (búsqueda, lotes, now-playing de una emisora, emisoras, estadísticas, series de salud) no llevan cabeceras de límite de frecuencia, porque el edge puede servir el mismo cuerpo a otro cliente. Tu saldo restante siempre está disponible en GET /v1/me.
Caché
Las respuestas en vivo se pueden almacenar en caché durante 5 segundos (Cache-Control: public, max-age=5, s-maxage=5) y llevan un ETag débil. Envía If-None-Match para recibir 304 cuando nada ha cambiado; un 304 no cuenta contra tu cuota en el origen. El directorio de emisoras se almacena en caché durante 60 segundos, una emisora individual durante 300. El historial es private, max-age=30. Los endpoints de salud y de claves son no-store.
Todas las respuestas llevan X-Request-Id. Indícalo cuando escribas al soporte.
Errores
Los errores siguen los detalles de problema de RFC 9457 con dos miembros de extensión: code, una cadena estable legible por máquina, y extras específicos de cada endpoint, como errors[] en los fallos de validación o required_tier en el 403.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://tuneping.com/docs/errors/#validation_failed",
"title": "Validation failed",
"status": 422,
"detail": "One or more query parameters are invalid.",
"instance": "urn:tuneping:request:req_01a18837c239727fbacb40d6c488405a",
"code": "validation_failed",
"errors": [ { "field": "q", "code": "length", "message": "`q` is required and must be 2–120 characters." } ]
}
La lista completa de códigos con su significado está en el catálogo de errores. Los primeros con los que te encontrarás:
| HTTP | code | Cuándo |
|---|---|---|
| 400 | invalid_request | Parámetro de consulta desconocido o solicitud mal formada |
| 400 | invalid_cursor | Cursor mal formado, caducado (24 h; 30 días para un since_cursor del feed de coincidencias) o usado con otros filtros |
| 401 | unauthenticated · invalid_api_key · key_revoked | Clave ausente, desconocida o revocada |
| 403 | forbidden_tier | El endpoint o el parámetro necesita un plan superior; required_tier indica cuál |
| 403 | forbidden_scope | La clave se emitió sin el ámbito; required_scope indica cuál |
| 404 | not_found · station_opted_out | Id desconocido, o la emisora pidió no ser indexada |
| 422 | validation_failed | Errores de campo en errors[] |
| 429 | rate_limited · quota_exceeded | Ve más despacio; Retry-After en segundos |
| 503 | unavailable | Base de datos no disponible, o una solicitud con un Idempotency-Key cancelada tras 20 segundos; reintenta pasado Retry-After |
GET/v1/now-playing/search sin clave public, max-age=5 · ETag
Búsqueda en vivo
Qué emisoras están poniendo q ahora mismo. Búsqueda aproximada que ignora los acentos: gronemeyer manner encuentra Grönemeyer – Männer. Las filas se ordenan por similitud y después por lo reciente que es la aparición del título. Sin paginación: el índice cambia cada pocos segundos, así que las páginas se saltarían o repetirían filas. Aumenta limit en su lugar.
| Parámetro | Tipo | Predeterminado | Significado |
|---|---|---|---|
q | string, 2–120 caracteres | obligatorio | Artista, título o ambos. Con tres o más caracteres se usa coincidencia por trigramas; las consultas más cortas buscan una subcadena. |
country | ISO 3166-1 alfa-2, repetible | cualquiera | Solo emisoras de estos países. |
codec | string | cualquiera | p. ej. MP3, AAC. |
min_bitrate | integer | 0 | kbit/s. |
classification | enum | song | song, ident, programme, ad_break, empty, junk, unknown o any. |
max_age_seconds | 30–900 | 180 | Solo filas cuya emisora se sondeó con éxito en este número de segundos. |
limit | 1–100 | 20 | Filas que se devuelven; truncated: true indica que había más. |
Respuesta · 200 · application/json
{
"data": [
{
"station": { "id": "159824f0-5475-4263-bcc8-59a48a213896", "name": "Radio 10 80's Hits", "country": "NL",
"codec": "MP3", "bitrate": 128, "is_hls": false,
"stream_url": "http://…/TLPSTR20.mp3", "homepage": "http://www.radio10.nl/", "recordability": "recordable" },
"raw_title": "Madonna - Holiday",
"artist": "Madonna", "title": "Holiday",
"track": { "id": "trk_c6f", "mbid": "d4708b79-62e7-4ce4-a6ca-247237d4ef4e", "artist_id": "art_fn" },
"classification": "song", "match_grade": "high", "confidence": "high",
"started_at": "2026-10-29T15:08:44.883Z", "start_uncertainty_seconds": 32,
"observed_at": "2026-10-29T15:09:26.631Z", "confirmed_at": "2026-10-29T15:10:30.679Z",
"method": "icy", "poll_interval_seconds": 60, "similarity": 0.5
}
],
"truncated": false,
"query": { "q": "madonna", "normalized": "madonna", "max_age_seconds": 180 }
}
Cómo leer una fila
- started_at RFC 3339 · estimación
- Inicio estimado del título actual, ±
start_uncertainty_secondsen cualquier sentido. - observed_at RFC 3339
- Cuándo vio Tuneping este título por primera vez.
- confirmed_at RFC 3339
- El último sondeo correcto de la emisora: «sigue sonando a esta hora». Úsalo para medir la frescura.
- track object · opcional
- Presente cuando el título se vinculó a una grabación de MusicBrainz.
- similarity 0–1
- Corta en tu propio umbral.
GET/v1/now-playing sin clave public, max-age=5 · ETag
Now-playing por lotes
El título actual de una lista de emisoras en una sola llamada, para pantallas de favoritas.
| Parámetro | Tipo | Predeterminado | Significado |
|---|---|---|---|
station_ids | UUID separados por comas | obligatorio | Hasta 20 en anónimo o Free, 100 a partir de Developer. Los ids se ordenan y se eliminan los duplicados antes de almacenar en caché, así que dos clientes con las mismas favoritas comparten un mismo cuerpo en caché. |
max_age_seconds | 30–86400 | 900 | Las filas más antiguas que este valor se siguen devolviendo, pero marcadas con is_stale: true. |
{ "data": [ { …same row shape as search, without similarity, plus "is_stale": false… } ],
"missing": [ "ids that are unknown or opted out" ],
"max_age_seconds": 900 }
GET/v1/stations/{id}/now-playing sin clave
Now-playing de una emisora
Una fila para una emisora. 404 not_found cuando todavía no se ha observado ningún título, 404 station_opted_out cuando la emisora pidió no ser indexada.
GET/v1/stations GET/v1/stations/{id} sin clave max-age=60 · 300
Emisoras
El directorio, construido a partir del directorio Radio Browser mantenido por la comunidad y enriquecido con la forma en que Tuneping lee cada emisora.
| Parámetro | Tipo | Predeterminado | Significado |
|---|---|---|---|
q | string | — | Nombre de la emisora, búsqueda aproximada. |
country | alfa-2, repetible | cualquiera | |
tag | string, repetible | cualquiera | Deben coincidir todas las etiquetas indicadas. |
codec, min_bitrate | Como en la búsqueda. | ||
has_now_playing | boolean | false | Solo emisoras de las que Tuneping lee títulos actualmente. |
sort | enum | clickcount | clickcount (popularidad, descendente), name, bitrate. |
limit | 1–200 | 50 | |
cursor | string opaco | — | Del next_cursor de la página anterior; válido 24 horas y solo con los mismos filtros. |
{ "data": [ {
"id": "159824f0-5475-4263-bcc8-59a48a213896", "name": "Radio 10 80's Hits", "country": "NL", "state": null,
"language": "dutch", "tags": ["80s", "hits"], "codec": "MP3", "bitrate": 128, "is_hls": false,
"stream_url": "http://…", "homepage": "http://www.radio10.nl/", "favicon_url": "https://…/favicon.png",
"clickcount": 1234, "recordability": "recordable",
"metadata": { "method": "icy", "poll_interval_seconds": 60, "lag_seconds": 9, "last_ok_at": "2026-10-29T15:10:30Z", "coverage": "live" },
"created_at": "2026-10-29T10:12:03Z", "updated_at": "2026-10-29T15:10:30Z" } ],
"next_cursor": "eyJ…" }
favicon_url se transmite tal cual como texto desde el directorio; Tuneping nunca copia ni sirve logotipos.
GET/v1/stations/{id}/history clave · developer + private, max-age=30
Historial de reproducciones
Las canciones que puso una emisora, las más recientes primero, con inicio y fin estimados. La profundidad depende del plan: 7 días en Developer, 12 meses en Pro y Business. Pedir un from más antiguo responde 403 forbidden_tier con required_tier, nunca una ventana recortada en silencio.
| Parámetro | Tipo | Predeterminado | Significado |
|---|---|---|---|
from, to | RFC 3339 | to = ahora, from = to − 24 h | Como máximo 31 días por llamada. |
classification | enum | song | Como en la búsqueda. |
limit | 1–200 | 50 | |
cursor | string opaco | — | La ventana queda fijada en el cursor. |
{ "data": [ {
"id": "pe_…", "station": { …station ref… },
"raw_title": "Hecht - Charlotta", "artist": "Hecht", "title": "Charlotta",
"track": { "id": "trk_…", "mbid": "…", "artist_id": "art_…" },
"classification": "song", "match_grade": "high",
"started_at": "2026-10-29T10:31:12Z", "start_uncertainty_seconds": 10,
"ended_at": "2026-10-29T10:34:40Z", "duration_seconds": 208,
"method": "icecast_json", "confidence": "high" } ],
"next_cursor": null,
"window": { "from": "2026-10-28T15:00:00Z", "to": "2026-10-29T15:00:00Z" } }
GET/v1/me GET/v1/me/usage clave no-store
Claves y uso
/v1/me devuelve la clave, el plan y el estado de su titular, los límites que se aplican y el uso hasta el momento en la ventana actual. /v1/me/usage?from=&to= devuelve las solicitudes por día (solo solicitudes al origen; los aciertos de la caché del edge no se cuentan).
{ "key": { "id": "key_…", "hint": "tp_live_a1b2c3…", "scopes": ["read", "exports", "webhooks", "wishlists"], "livemode": true,
"created_at": "…", "last_used_at": "…" },
"owner": { "id": "own_…", "tier": "developer", "status": "active",
"current_period_start": "…", "current_period_end": "…" },
"limits": { "requests_per_second": 10, "burst": 20, "requests_per_day": null, "requests_per_month": 100000,
"history_days": 7, "batch_station_ids": 100, "webhooks": 0, "wishlists": 10,
"sse_connections": 1, "tightened_stations": 200 },
"usage": { "period_requests": 41230, "today_requests": 1810, "resets_at": "…" } }
GET/v1/health GET/v1/health/series GET/v1/stats sin clave
Salud y estadísticas
/v1/health responde 200 con status ok o degraded, y 503 cuando la base de datos está caída; apunta a él tu monitor de disponibilidad. /v1/health/series?minutes=60 (5–1440) devuelve cifras del sondeador por minuto: polls, ok, titles_changed, songs, bytes_in, bytes_out, latency_p50_ms, latency_p90_ms, inflight_max, ips_paused, queue_lag_seconds y un desglose by_method. /v1/stats es la instantánea pública de cobertura que se muestra en la página de estado: emisoras en total, sondeadas, con metadatos en vivo, cambios de título por minuto, proporción de canciones, por método y por país.
POST/v1/wishlists PUT/v1/wishlists GET/v1/wishlists/matches clave · developer + no-store
Listas de deseos
Indica una canción, un artista o un título en texto libre y entérate de cuándo una emisora empieza a ponerlo. Cada cambio de título se compara con tus listas de deseos en el momento en que se lee, y una lista de deseos nueva está activa en menos de 30 segundos. Developer admite 10 listas de deseos, Pro 100, Business 1.000. Las coincidencias se conservan 30 días.
| Campo | Tipo | Significado |
|---|---|---|
kind | artist · track · query | artist y track toman un id de la búsqueda (art_…, trk_…) y también capturan las grafías que el normalizador vincula con él. query toma texto libre, normalmente Artist - Title. |
value | string, ≤ 240 | El id o el texto. |
match | exact · fuzzy | Solo query. Exact compara el texto normalizado; fuzzy acepta una similitud por trigramas de 0,6 o más. |
station_filter | object | station_ids (≤ 500) y/o countries (alfa-2 en mayúsculas o minúsculas, devuelto en mayúsculas). Un título coincide cuando su emisora está en la lista o pertenece a un país de la lista. Sin filtro, se incluyen todas las emisoras. |
delivery | array | poll (predeterminado), sse, webhook. La entrega por webhook es de Pro en adelante y necesita webhook_id y una clave con el ámbito webhooks. |
description | string, ≤ 200 | Para tu uso. |
curl -H "Authorization: Bearer tp_live_…" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"query","value":"Daft Punk - One More Time","station_filter":{"countries":["FR","BE"]}}' \
https://api.tuneping.com/v1/wishlists
Una lista explícita de station_ids hace que Tuneping lea esas emisoras cada 30 segundos en lugar de cada 60 mientras la lista de deseos esté activada, y la respuesta lo indica con tightens: true. La unión de las emisoras que listas tiene un tope por plan (limits.tightened_stations en /v1/me); una lista de deseos que lo superaría responde 422 validation_failed con una entrada en errors[] cuyo code es tightened_stations_exceeded (campo station_filter.station_ids, o data en PUT). Los filtros por país siguen generando coincidencias, al intervalo normal de cada emisora.
GET /v1/wishlists/{id}, PATCH (filtro de emisoras, match, entrega, webhook, descripción, status: enabled | paused) y DELETE funcionan como cabe esperar. PUT /v1/wishlists con {"data": [ … ]} sustituye todo tu conjunto en una sola llamada: los elementos sin cambios conservan su id y su recuento de coincidencias, el resto se crean o se eliminan. Es la forma más sencilla de sincronizar una lista que tu app guarda localmente. Su cuerpo puede ocupar hasta 1 MiB; los demás cuerpos de solicitud están limitados a 64 KiB (413 payload_too_large).
Las coincidencias son un feed de cola. GET /v1/wishlists/matches abarca todas tus listas de deseos y GET /v1/wishlists/{id}/matches una de ellas. Las filas llegan de la más antigua a la más reciente. Guarda el next_cursor de cada respuesta y devuélvelo como since_cursor: nunca es null, sigue siendo válido durante los 30 días de retención y sobrevive a los reinicios. Sin cursor recibes las coincidencias más recientes. limit va de 1 a 500, con 100 por defecto.
{ "data": [ {
"id": "wm_…", "wishlist_id": "wl_…",
"station": { "id": "…", "name": "…", "country": "FR", "codec": "MP3", "bitrate": 128, "is_hls": false,
"stream_url": "https://…", "homepage": "https://…", "recordability": "recordable" },
"raw_title": "Daft Punk - One More Time", "track": { "id": "trk_…", "mbid": "…", "artist_id": "art_…" },
"started_at": "2026-10-30T21:04:10Z", "start_uncertainty_seconds": 10,
"observed_at": "2026-10-30T21:04:20Z", "matched_at": "2026-10-30T21:04:20Z",
"confidence": "high", "expected_end_hint_at": "2026-10-30T21:07:52Z" } ],
"next_cursor": "eyJ0Ijo…" }
started_at ya está corregido según el momento en que se vio el título, así que una grabadora puede mostrar «empezó hace unos N ± U segundos». expected_end_hint_at es started_at más 222 segundos, la duración mediana de una canción: una pista, no una predicción. confidence es high cuando la coincidencia pasó por un id de pista o de artista vinculado, medium para el artista y el título normalizados, y low para las consultas de texto.
POST/v1/webhooks GET/v1/webhooks/{id}/deliveries clave · pro + no-store
Webhooks
Tuneping envía eventos por POST a tu endpoint HTTPS a medida que ocurren. Pro tiene 5 endpoints, Business 20.
| Evento | Cuándo | Pro necesita |
|---|---|---|
wishlist.matched | Hubo una coincidencia en una lista de deseos que indica este endpoint en webhook_id. | — |
station.title_changed | Cambió la línea de título de una emisora. | filters.station_ids (≤ 200) |
track.started | Empezó una pista vinculada. | filters.track_ids o artist_ids (≤ 200) |
artist.started | Empezó un artista vinculado. | filters.track_ids o artist_ids (≤ 200) |
Los filtros también pueden incluir countries y classification. Los filtros solo por país y los flujos sin filtrar de cambios de título son de Business. Cada endpoint tiene un presupuesto diario de 50.000 eventos en Pro y 500.000 en Business; si se supera, el endpoint pasa a paused_budget hasta el siguiente día UTC y recibes un correo.
curl -H "Authorization: Bearer tp_live_…" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/tuneping","events":["wishlist.matched"]}' \
https://api.tuneping.com/v1/webhooks
La respuesta incluye el secret de firma (whsec_…) una sola vez. Las entregas siguen Standard Webhooks: cabeceras webhook-id, webhook-timestamp y webhook-signature: v1,<base64>, donde la firma es un HMAC-SHA256 con los bytes del secreto sobre {webhook-id}.{webhook-timestamp}.{body}. Rechaza las marcas de tiempo desfasadas más de 5 minutos y usa webhook-id para descartar duplicados: se mantiene igual entre reintentos.
{ "id": "evt_…", "type": "wishlist.matched", "created_at": "2026-10-30T21:04:20Z",
"data": { "station_id": "…", "station": { …station ref… }, "wishlist_id": "wl_…", "match_id": "wm_…",
"raw_title": "Daft Punk - One More Time", "artist": "Daft Punk", "title": "One More Time",
"track_id": "trk_…", "artist_id": "art_…", "classification": "song",
"started_at": "…", "start_uncertainty_seconds": 10, "observed_at": "…",
"confidence": "high", "expected_end_hint_at": "…" },
"links": { "station": "/v1/stations/…", "wishlist": "/v1/wishlists/wl_…" } }
Responde con cualquier 2xx en menos de 10 segundos. Las redirecciones no se siguen. Las entregas fallidas se reintentan pasados 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h y 24 h, unos tres días en total, de una en una por endpoint. Un endpoint que responde 410, o que falla cinco días seguidos, se desactiva y recibes un correo; vuelve a poner status en enabled con PATCH cuando esté arreglado.
POST /v1/webhooks/{id}/rotate-secret devuelve un secreto nuevo; durante 24 horas cada entrega lleva dos firmas, una por secreto. POST /v1/webhooks/{id}/test envía un ping. GET /v1/webhooks/{id}/deliveries?status=pending|succeeded|failed lista 30 días de entregas, y POST …/deliveries/{dl_id}/redeliver vuelve a enviar una. Las claves de prueba (tp_test_…) solo pueden apuntar a webhook.site.
GET/v1/stream clave · developer + text/event-stream
Flujo de eventos
Los mismos eventos a través de una única respuesta HTTP larga en formato server-sent events, para servidores y apps nativas. Pasa station_ids (hasta 100) o wishlist_id (all o un id wl_…).
curl -N -H "Authorization: Bearer tp_live_…" -H "Accept: text/event-stream" \
"https://api.tuneping.com/v1/stream?station_ids=159824f0-5475-4263-bcc8-59a48a213896"
retry: 3000
id: evt_1m675e1tkfdxtn7axmq6qktz8
event: station.title_changed
data: {"id":"evt_1m675e1tkfdxtn7axmq6qktz8","type":"station.title_changed","created_at":"…","data":{…}}
: ping
| Parámetro | Predeterminado | Significado |
|---|---|---|
station_ids | — | Ids de emisoras separados por comas, como máximo 100. |
wishlist_id | — | all o un id de lista de deseos. Envía wishlist.matched. |
events | station.title_changed | Para flujos de emisoras, cualquiera de station.title_changed, track.started, artist.started. |
classification | song | song o any, para flujos de emisoras. |
data es el mismo JSON que recibe un webhook. Un comentario : ping llega tras 20 segundos sin eventos, así que configura el tiempo de inactividad de tu cliente en 60 segundos o más. Cuando se corte la conexión, vuelve a conectarte con la cabecera Last-Event-ID fijada en el último id que viste: se reenvían los eventos de los últimos 10 minutos. Una posición más antigua recibe un event: gap con el id disponible más antiguo; rellena el hueco con el historial o con el feed de coincidencias. Flujos abiertos por clave: Developer 1, Pro 5, Business 20, Tapelark 1 por dispositivo. Uno más responde 429 too_many_connections.
Los navegadores no pueden usar EventSource aquí, porque no puede enviar la cabecera Authorization y las claves nunca se aceptan en la URL. Lee el flujo con fetch() en su lugar.
Campos y enums
- classification enum
song·ident(nombre o eslogan de la emisora) ·programme·ad_break·empty·junk·unknown. Solo las canciones pasan al historial de reproducciones; todo se conserva en la fila en vivo.- match_grade enum
high·medium·none. Qué tan bien se vinculó el título a una grabación de MusicBrainz.- confidence enum
high·medium·low. Se calcula por fila a partir de la clasificación, el grado de coincidencia, lo reciente que es la última confirmación de la emisora y su historial de fiabilidad. Está documentado para que puedas auditarlo.- method enum
- Cómo lee Tuneping la emisora:
icy(metadatos dentro del stream),icecast_json(JSON de estado del servidor),shoutcast_stats(XML de estado),shoutcast_currentsong,seven_html. Son identificadores internos, no avales; si los muestras, preséntalos como «en el stream» y «página de estado». - recordability enum
- Si el stream de la emisora es un stream HTTP simple y grabable desde el punto de vista de un cliente de grabación. Informativo.
- Ids
- Las emisoras usan su UUID de Radio Browser. Las pistas son
trk_…, los artistasart_…, los eventos de reproducciónpe_…, las claveskey_…, los titularesown_…, las listas de deseoswl_…, las coincidenciaswm_…, los webhookswh_…, las entregasdl_…, los eventosevt_…. Trátalos todos como cadenas opacas. - Marcas de tiempo
- RFC 3339, UTC, con precisión de milisegundos cuando la fuente la tiene.
OpenAPI
La descripción legible por máquina de esta API está en /docs/openapi-v1.yaml (OpenAPI 3.1). Es la fuente para generar clientes; los ids de operación son estables.
Atribución y uso razonable
El uso del plan Free y el uso anónimo muestran un enlace visible «Data by Tuneping» donde se presentan los datos (insignia). Las reproducciones se detectan a partir de los metadatos de las emisoras y no son una monitorización certificada; indica «detectado a partir de los metadatos de la emisora» cuando las presentes como prueba. Los nombres de las emisoras son texto; Tuneping no proporciona logotipos ni imágenes. Consulta las condiciones.