Tuneping API v1
JSON API do sprawdzania, co teraz grają internetowe stacje radiowe i co grały wcześniej. Bazowy URL https://api.tuneping.com/v1. Tylko metadane: usługa czyta publiczny wiersz tytułu każdego strumienia i nigdy nie dotyka dźwięku.
Szybki start
Kto teraz gra Madonnę?
curl "https://api.tuneping.com/v1/now-playing/search?q=madonna&limit=3"
Co teraz gra na stacji, której id znasz (id to UUID stacji z Radio Browser):
curl "https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/now-playing"
Co stacja grała w ciągu ostatnich 24 godzin (wymaga klucza z dostępem do historii, plan Developer lub wyższy):
curl -H "Authorization: Bearer tp_live_…" \
"https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/history"
Każda odpowiedź to JSON. Błędy mają typ application/problem+json (zob. Błędy). Nieznane parametry zapytania są odrzucane z kodem 400, a nie ignorowane, więc literówki wychodzą od razu.
Jak uzyskać klucz
Samodzielnie na tuneping.com/signup: wpisz adres e-mail, otwórz link z wiadomości w ciągu 60 minut, a klucz zostanie pokazany jeden raz. Te same dwa kroki są dostępne jako API: POST /v1/signup z {"email": "…", "plan": "free|developer|pro"} odpowiada 202 i wysyła wiadomość; POST /v1/signup/verify z {"token": "…"} z linku odpowiada 201 z kluczem. Plan Free nie wymaga niczego więcej. Developer i Pro przechodzą do Stripe Checkout przez link w odpowiedzi; plan właściciela zmienia się w ciągu minuty od płatności, a wszystkie klucze właściciela idą za nim.
Zgubiony klucz? Zarejestruj się ponownie na ten sam adres: klucz zostanie wymieniony, a stary działa jeszcze przez 24 godziny. Jeden aktywny klucz w planach Free i Developer, pięć w Pro, dwadzieścia w Business; do tego w każdym planie jeden klucz tp_test_, wydawany na prośbę, dopóki nie powstanie panel.
Uwierzytelnianie
Wysyłaj klucz jako token bearer albo w X-Api-Key:
Authorization: Bearer tp_live_a1b2c3…
X-Api-Key: tp_live_a1b2c3…
Klucze zaczynają się od tp_live_ lub tp_test_, po których następują 43 znaki. Żądanie bez klucza jest traktowane jako anonimowe i ograniczane według adresu IP. Niepoprawny, nieznany lub unieważniony klucz od razu daje odpowiedź 401, nawet na endpointach, które działałyby anonimowo. Endpointy oznaczone niżej jako klucz bez klucza odpowiadają 401 unauthenticated.
Zakresy. Klucze mają wszystkie zakresy (read, exports, webhooks, wishlists), chyba że wydano je z węższą listą, na przykład tylko do odczytu; GET /v1/me podaje zakresy klucza. Listy życzeń i webhooki wymagają zakresów wishlists i webhooks (lista życzeń z dostarczaniem webhook wymaga obu, sprawdzanych przed planem). Klucz bez danego zakresu odpowiada 403 forbidden_scope z required_scope, a WWW-Authenticate zawiera error="insufficient_scope".
Klucze testowe. Klucz tp_test_ czyta te same dane na żywo z limitami planu Free i ma własne listy życzeń, dopasowania i endpointy webhooków (livemode: false). Nigdy nie widzi ani nie zmienia tych produkcyjnych, więc zestaw testów może wykonać PUT z pustym zbiorem list życzeń bez naruszania produkcji. Webhooki w trybie testowym mogą wskazywać tylko na webhook.site.
Ponowienia. POST, PATCH i DELETE na endpointach, które wymagają klucza lub tokenu partnera, przyjmują nagłówek Idempotency-Key (jeden UUID na logiczne żądanie); endpointy rejestracji go nie przyjmują. Ponowienie z tym samym kluczem i tym samym żądaniem dostaje ponownie pierwszą udaną odpowiedź, oznaczoną Idempotent-Replayed: true, zamiast tworzyć drugą listę życzeń lub drugi endpoint. Klucz jest pamiętany przez 24 godziny; użycie go dla innego żądania daje 422 idempotency_key_reused, a ponowienie, gdy pierwsze żądanie wciąż trwa, daje 409 idempotency_in_progress. Błędy nie są zapamiętywane, więc ponowienie po błędzie wykonuje się od nowa. Zerwane połączenie nie anuluje żądania z tym nagłówkiem: żądanie kończy się, a udana odpowiedź zostaje zapamiętana, więc Twoje ponowienie ją dostanie. Żądanie, które wciąż trwa po 20 sekundach, jest anulowane bez żadnych zmian i odpowiada 503 unavailable z Retry-After: 1; ponów je z tym samym kluczem.
Aplikacje partnerskie (obecnie Tapelark) zamiast klucza wysyłają podpisany token uprawnień: Authorization: Bearer TPE1-…. Token jest weryfikowany offline względem kluczy Ed25519 partnera i odpowiada planowi partnera z tabeli poniżej; API nigdy nie dowiaduje się, kim jest użytkownik końcowy. Niepoprawny lub nieznany token daje 401 token_invalid, wygasły 401 token_expired (odśwież go), token dla innego odbiorcy 401 token_audience, a wygasłe lub unieważnione uprawnienie 403 entitlement_revoked.
Limity szybkości i przydziały
| Plan | Tempo | Przydział | Id w żądaniu zbiorczym | Historia |
|---|---|---|---|---|
| Anonimowo (na IP) | 2 req/s, seria 5 | 1000 / dzień | 20 | brak |
| Free | 2 req/s, seria 5 | 1000 / dzień | 20 | brak |
| Developer | 10 req/s, seria 20 | 100 000 / miesiąc | 100 | 7 dni |
| Pro | 50 req/s, seria 100 | 1 000 000 / miesiąc | 100 | 12 miesięcy |
| Business | indywidualnie | indywidualnie | 100 | 12 miesięcy |
| Tapelark (token uprawnień, na licencję) | 5 req/s, seria 10 | 2000 / dzień | 50 | 7 dni |
Limit na sekundę działa jak kubełek tokenów. Gdy kubełek jest pusty, dostajesz 429 rate_limited z Retry-After. Gdy przydział jest wykorzystany, dostajesz 429 quota_exceeded z Retry-After ustawionym na liczbę sekund do odnowienia okna.
Uwierzytelnione odpowiedzi, których nie można współdzielić w cache, zawierają pola limitów szybkości IETF:
RateLimit-Policy: "burst";q=10;w=1, "monthly";q=100000;w=2592000
RateLimit: "burst";r=7;t=1
Odpowiedzi, które można współdzielić w cache (wyszukiwanie, zbiorcze „teraz gra”, „teraz gra” dla stacji, stacje, statystyki, seria stanu), nie mają nagłówków limitów szybkości, bo serwer brzegowy może podać tę samą treść innemu wywołującemu. Pozostały budżet zawsze sprawdzisz pod GET /v1/me.
Pamięć podręczna
Odpowiedzi na żywo można przechowywać w cache przez 5 sekund (Cache-Control: public, max-age=5, s-maxage=5) i mają słaby ETag. Wyślij If-None-Match, by dostać 304, gdy nic się nie zmieniło; 304 nie wlicza się do Twojego przydziału na serwerze źródłowym. Katalog stacji jest w cache 60 sekund, pojedyncza stacja 300. Historia ma private, max-age=30. Endpointy stanu i kluczy mają no-store.
Każda odpowiedź zawiera X-Request-Id. Podawaj go, gdy piszesz do wsparcia.
Błędy
Błędy są zgodne ze szczegółami problemu z RFC 9457 i mają dwa pola rozszerzeń: code, stały ciąg do odczytu maszynowego, oraz dodatki zależne od endpointu, takie jak errors[] przy błędach walidacji albo required_tier przy 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." } ]
}
Pełna lista kodów wraz ze znaczeniem jest w katalogu błędów. Te, które spotkasz najpierw:
| HTTP | code | Kiedy |
|---|---|---|
| 400 | invalid_request | Nieznany parametr zapytania lub niepoprawne żądanie |
| 400 | invalid_cursor | Kursor niepoprawny, wygasły (24 h; 30 dni dla since_cursor kanału dopasowań) lub użyty z innymi filtrami |
| 401 | unauthenticated · invalid_api_key · key_revoked | Brak klucza, klucz nieznany lub unieważniony |
| 403 | forbidden_tier | Endpoint lub parametr wymaga wyższego planu; required_tier podaje którego |
| 403 | forbidden_scope | Klucz wydano bez tego zakresu; required_scope podaje którego |
| 404 | not_found · station_opted_out | Nieznane id albo stacja poprosiła, by jej nie indeksować |
| 422 | validation_failed | Błędy pól w errors[] |
| 429 | rate_limited · quota_exceeded | Zwolnij; Retry-After w sekundach |
| 503 | unavailable | Baza danych niedostępna albo żądanie z Idempotency-Key anulowane po 20 sekundach; ponów po Retry-After |
GET/v1/now-playing/search bez klucza public, max-age=5 · ETag
Wyszukiwanie na żywo
Które stacje grają teraz q. Wyszukiwanie rozmyte i niezależne od znaków diakrytycznych: gronemeyer manner znajduje Grönemeyer – Männer. Wiersze są posortowane według podobieństwa, a potem według tego, jak niedawno pojawił się tytuł. Bez stronicowania: indeks zmienia się co kilka sekund, więc strony pomijałyby lub powtarzały wiersze. Zamiast tego zwiększ limit.
| Parametr | Typ | Domyślnie | Znaczenie |
|---|---|---|---|
q | string, 2–120 znaków | wymagany | Wykonawca, tytuł lub oba. Od trzech znaków używane jest dopasowanie trigramowe; krótsze zapytania to dopasowanie podciągu. |
country | ISO 3166-1 alpha-2, powtarzalny | dowolny | Tylko stacje z tych krajów. |
codec | string | dowolny | np. MP3, AAC. |
min_bitrate | integer | 0 | kbit/s. |
classification | enum | song | song, ident, programme, ad_break, empty, junk, unknown lub any. |
max_age_seconds | 30–900 | 180 | Tylko wiersze, których stacja została pomyślnie odpytana w ciągu tylu sekund. |
limit | 1–100 | 20 | Liczba zwracanych wierszy; truncated: true oznacza, że było ich więcej. |
Odpowiedź · 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 }
}
Jak czytać wiersz
- started_at RFC 3339 · szacunek
- Szacowany początek bieżącego tytułu, ±
start_uncertainty_secondsw każdą stronę. - observed_at RFC 3339
- Kiedy Tuneping po raz pierwszy zobaczył ten tytuł.
- confirmed_at RFC 3339
- Ostatnie udane odpytanie stacji: „nadal gra według stanu na”. Używaj go do oceny świeżości.
- track object · opcjonalny
- Obecny, gdy tytuł połączono z nagraniem w MusicBrainz.
- similarity 0–1
- Odcinaj według własnego progu.
GET/v1/now-playing bez klucza public, max-age=5 · ETag
Zbiorcze „teraz gra”
Bieżący tytuł dla listy stacji w jednym wywołaniu, na ekrany ulubionych.
| Parametr | Typ | Domyślnie | Znaczenie |
|---|---|---|---|
station_ids | UUID rozdzielone przecinkami | wymagany | Do 20 anonimowo lub w Free, 100 od planu Developer. Przed zapisaniem w cache id są sortowane i deduplikowane, więc dwóch klientów z tymi samymi ulubionymi korzysta z jednej treści w cache. |
max_age_seconds | 30–86400 | 900 | Starsze wiersze są nadal zwracane, ale oznaczone 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 bez klucza
„Teraz gra” dla stacji
Jeden wiersz dla jednej stacji. 404 not_found, gdy nie zaobserwowano jeszcze żadnego tytułu, 404 station_opted_out, gdy stacja poprosiła, by jej nie indeksować.
GET/v1/stations GET/v1/stations/{id} bez klucza max-age=60 · 300
Stacje
Katalog zbudowany na podstawie prowadzonego przez społeczność katalogu Radio Browser i uzupełniony o to, jak Tuneping czyta każdą stację.
| Parametr | Typ | Domyślnie | Znaczenie |
|---|---|---|---|
q | string | — | Nazwa stacji, rozmyte. |
country | alpha-2, powtarzalny | dowolny | |
tag | string, powtarzalny | dowolny | Muszą pasować wszystkie podane tagi. |
codec, min_bitrate | Jak w wyszukiwaniu. | ||
has_now_playing | boolean | false | Tylko stacje, z których Tuneping obecnie czyta tytuły. |
sort | enum | clickcount | clickcount (popularność, malejąco), name, bitrate. |
limit | 1–200 | 50 | |
cursor | nieprzezroczysty string | — | Z next_cursor poprzedniej strony; ważny 24 godziny i tylko z tymi samymi filtrami. |
{ "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 jest przekazywany z katalogu jako tekst; Tuneping nigdy nie kopiuje ani nie serwuje logo.
GET/v1/stations/{id}/history klucz · developer + private, max-age=30
Historia odtworzeń
Utwory zagrane przez stację, od najnowszych, z szacowanym początkiem i końcem. Głębokość zależy od planu: 7 dni w Developer, 12 miesięcy w Pro i Business. Prośba o starszy from daje 403 forbidden_tier z required_tier, nigdy po cichu przycięte okno.
| Parametr | Typ | Domyślnie | Znaczenie |
|---|---|---|---|
from, to | RFC 3339 | to = teraz, from = to − 24 h | Najwyżej 31 dni na wywołanie. |
classification | enum | song | Jak w wyszukiwaniu. |
limit | 1–200 | 50 | |
cursor | nieprzezroczysty string | — | Okno jest zapisane na stałe w kursorze. |
{ "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 klucz no-store
Klucze i wykorzystanie
/v1/me zwraca klucz, plan i status jego właściciela, obowiązujące limity i dotychczasowe wykorzystanie w bieżącym oknie. /v1/me/usage?from=&to= zwraca liczbę żądań na dzień (tylko żądania do serwera źródłowego; trafienia w cache na serwerach brzegowych nie są liczone).
{ "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 bez klucza
Stan i statystyki
/v1/health odpowiada 200 ze status równym ok lub degraded, a 503, gdy baza danych nie działa; skieruj na niego swój monitoring dostępności. /v1/health/series?minutes=60 (5–1440) zwraca minutowe dane pollera: polls, ok, titles_changed, songs, bytes_in, bytes_out, latency_p50_ms, latency_p90_ms, inflight_max, ips_paused, queue_lag_seconds oraz podział by_method. /v1/stats to publiczny przegląd pokrycia pokazywany na stronie statusu: liczba stacji ogółem, odpytywanych, z metadanymi na żywo, zmiany tytułów na minutę, udział piosenek, według metody, według kraju.
POST/v1/wishlists PUT/v1/wishlists GET/v1/wishlists/matches klucz · developer + no-store
Listy życzeń
Podaj utwór, wykonawcę lub dowolny tytuł tekstowy i dowiedz się, kiedy jakaś stacja zacznie go grać. Każda zmiana tytułu jest sprawdzana względem Twoich list życzeń w chwili odczytu, a nowa lista życzeń działa w ciągu 30 sekund. Developer może mieć 10 list życzeń, Pro 100, Business 1000. Dopasowania są przechowywane 30 dni.
| Pole | Typ | Znaczenie |
|---|---|---|
kind | artist · track · query | artist i track przyjmują id z wyszukiwania (art_…, trk_…) i łapią też zapisy, które normalizator łączy z tym id. query przyjmuje dowolny tekst, zwykle Artist - Title. |
value | string, ≤ 240 | Id lub tekst. |
match | exact · fuzzy | Tylko query. Exact porównuje znormalizowany tekst; fuzzy akceptuje podobieństwo trigramowe 0,6 lub wyższe. |
station_filter | object | station_ids (≤ 500) i/lub countries (alpha-2 w dowolnej wielkości liter, zwracane wielkimi literami). Tytuł pasuje, gdy jego stacja jest na liście lub leży w wymienionym kraju. Brak filtra oznacza wszystkie stacje. |
delivery | array | poll (domyślnie), sse, webhook. Dostarczanie webhookiem jest dostępne od planu Pro i wymaga webhook_id oraz klucza z zakresem webhooks. |
description | string, ≤ 200 | Twój opis. |
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
Jawna lista station_ids sprawia, że Tuneping czyta te stacje co 30 sekund zamiast co 60, dopóki lista życzeń jest włączona, a odpowiedź informuje o tym przez tightens: true. Suma wymienionych przez Ciebie stacji ma limit zależny od planu (limits.tightened_stations w /v1/me); lista życzeń, która by go przekroczyła, daje 422 validation_failed z wpisem w errors[], którego code to tightened_stations_exceeded (pole station_filter.station_ids albo data przy PUT). Filtry krajów nadal działają, w normalnym interwale każdej stacji.
GET /v1/wishlists/{id}, PATCH (filtr stacji, match, dostarczanie, webhook, opis, status: enabled | paused) i DELETE działają zgodnie z oczekiwaniami. PUT /v1/wishlists z {"data": [ … ]} zastępuje cały Twój zbiór w jednym wywołaniu: niezmienione pozycje zachowują swoje id i licznik dopasowań, pozostałe są tworzone lub usuwane. To najprostszy sposób synchronizacji listy, którą Twoja aplikacja trzyma lokalnie. Jego treść może mieć do 1 MiB; treść innych żądań jest ograniczona do 64 KiB (413 payload_too_large).
Dopasowania to kanał czytany od końca (tail). GET /v1/wishlists/matches obejmuje wszystkie Twoje listy życzeń, a GET /v1/wishlists/{id}/matches jedną z nich. Wiersze przychodzą od najstarszych. Zachowuj next_cursor z każdej odpowiedzi i odsyłaj go jako since_cursor: nigdy nie jest null, pozostaje ważny przez 30-dniowy okres przechowywania i przetrwa restarty. Bez kursora dostajesz najnowsze dopasowania. limit to 1–500, domyślnie 100.
{ "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 jest już skorygowany o moment, w którym zobaczono tytuł, więc aplikacja nagrywająca może pokazać „zaczęło się około N ± U sekund temu”. expected_end_hint_at to started_at plus 222 sekundy, czyli mediana długości utworu: wskazówka, nie prognoza. confidence ma wartość high, gdy dopasowanie przeszło przez połączone id utworu lub wykonawcy, medium dla znormalizowanego wykonawcy i tytułu, low dla zapytań tekstowych.
POST/v1/webhooks GET/v1/webhooks/{id}/deliveries klucz · pro + no-store
Webhooki
Tuneping wysyła zdarzenia na Twój endpoint HTTPS na bieżąco. Pro ma 5 endpointów, Business 20.
| Zdarzenie | Kiedy | Wymagane w Pro |
|---|---|---|
wishlist.matched | Pasuje lista życzeń, która wskazuje ten endpoint w webhook_id. | — |
station.title_changed | Zmienił się wiersz tytułu stacji. | filters.station_ids (≤ 200) |
track.started | Zaczął się połączony utwór. | filters.track_ids lub artist_ids (≤ 200) |
artist.started | Zaczął się utwór połączonego wykonawcy. | filters.track_ids lub artist_ids (≤ 200) |
Filtry mogą też zawierać countries i classification. Filtry tylko po kraju i niefiltrowane strumienie zmian tytułów są dostępne w Business. Każdy endpoint ma dzienny budżet 50 000 zdarzeń w Pro i 500 000 w Business; po jego przekroczeniu endpoint ma stan paused_budget do następnego dnia UTC, a Ty dostajesz e-mail.
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
Odpowiedź zawiera secret do podpisywania (whsec_…), jeden raz. Dostarczenia są zgodne ze Standard Webhooks: nagłówki webhook-id, webhook-timestamp i webhook-signature: v1,<base64>, gdzie podpis to HMAC-SHA256 z bajtami sekretu jako kluczem, liczony z {webhook-id}.{webhook-timestamp}.{body}. Odrzucaj znaczniki czasu odbiegające o więcej niż 5 minut i używaj webhook-id do odrzucania duplikatów: pozostaje ten sam przy ponowieniach.
{ "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_…" } }
Odpowiedz dowolnym kodem 2xx w ciągu 10 sekund. Tuneping nie podąża za przekierowaniami. Nieudane dostarczenia są ponawiane po 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h i 24 h, łącznie przez około trzy dni, po jednym naraz na endpoint. Endpoint, który odpowiada 410 albo zawodzi przez pięć dni z rzędu, zostaje wyłączony, a Ty dostajesz e-mail; po naprawie ustaw status z powrotem na enabled przez PATCH.
POST /v1/webhooks/{id}/rotate-secret zwraca nowy sekret; przez 24 godziny każde dostarczenie ma dwa podpisy, po jednym na sekret. POST /v1/webhooks/{id}/test wysyła ping. GET /v1/webhooks/{id}/deliveries?status=pending|succeeded|failed wypisuje dostarczenia z 30 dni, a POST …/deliveries/{dl_id}/redeliver wysyła jedno ponownie. Klucze testowe (tp_test_…) mogą wskazywać tylko na webhook.site.
GET/v1/stream klucz · developer + text/event-stream
Strumień zdarzeń
Te same zdarzenia w jednej długiej odpowiedzi HTTP w formacie server-sent events, dla serwerów i aplikacji natywnych. Przekaż albo station_ids (do 100), albo wishlist_id (all lub jedno 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
| Parametr | Domyślnie | Znaczenie |
|---|---|---|
station_ids | — | Id stacji rozdzielone przecinkami, najwyżej 100. |
wishlist_id | — | all lub id listy życzeń. Wysyła wishlist.matched. |
events | station.title_changed | Dla strumieni stacji dowolne z station.title_changed, track.started, artist.started. |
classification | song | song lub any, dla strumieni stacji. |
data to ten sam JSON, który dostaje webhook. Komentarz : ping przychodzi po 20 sekundach bez zdarzenia, więc ustaw limit bezczynności klienta na 60 sekund lub więcej. Gdy połączenie się zerwie, połącz się ponownie z nagłówkiem Last-Event-ID ustawionym na ostatnie widziane id: zdarzenia z ostatnich 10 minut zostaną odtworzone. Starsza pozycja dostaje event: gap z najwcześniejszym dostępnym id; uzupełnij lukę z historii lub kanału dopasowań. Otwarte strumienie na klucz: Developer 1, Pro 5, Business 20, Tapelark 1 na urządzenie. Kolejny daje 429 too_many_connections.
Przeglądarki nie mogą tu używać EventSource, bo nie potrafi on wysłać nagłówka Authorization, a klucze nigdy nie są przyjmowane w URL. Zamiast tego czytaj strumień przez fetch().
Pola i wartości enum
- classification enum
song·ident(nazwa lub slogan stacji) ·programme·ad_break·empty·junk·unknown. Do historii odtworzeń trafiają tylko piosenki; w wierszu na żywo zachowywane jest wszystko.- match_grade enum
high·medium·none. Jak dobrze tytuł połączono z nagraniem w MusicBrainz.- confidence enum
high·medium·low. Obliczana dla każdego wiersza z klasyfikacji, jakości dopasowania, tego, jak niedawno potwierdzono stację, i dotychczasowej niezawodności stacji. Udokumentowana, aby można ją było zweryfikować.- method enum
- Jak Tuneping czyta stację:
icy(metadane w strumieniu),icecast_json(JSON statusu serwera),shoutcast_stats(XML statusu),shoutcast_currentsong,seven_html. Wewnętrzne identyfikatory, a nie rekomendacje; jeśli je pokazujesz, wyświetlaj je jako „w strumieniu” i „strona statusu”. - recordability enum
- Czy strumień stacji jest, z perspektywy klienta nagrywającego, zwykłym strumieniem HTTP, który da się nagrać. Informacyjnie.
- Id
- Stacje używają swojego UUID z Radio Browser. Utwory to
trk_…, wykonawcyart_…, zdarzenia odtworzeńpe_…, kluczekey_…, właścicieleown_…, listy życzeńwl_…, dopasowaniawm_…, webhookiwh_…, dostarczeniadl_…, zdarzeniaevt_…. Traktuj je wszystkie jako nieprzezroczyste ciągi znaków. - Znaczniki czasu
- RFC 3339, UTC, z precyzją do milisekund, jeśli źródło ją ma.
OpenAPI
Opis tego API do odczytu maszynowego znajduje się pod /docs/openapi-v1.yaml (OpenAPI 3.1). Jest źródłem do generowania klientów; identyfikatory operacji są stałe.
Oznaczenie źródła i uczciwe korzystanie
Korzystanie w planie Free i anonimowe wymaga widocznego linku „Data by Tuneping” tam, gdzie wyświetlane są dane (plakietka). Odtworzenia są wykrywane na podstawie metadanych stacji i nie są certyfikowanym monitoringiem; tam, gdzie przedstawiasz je jako dowód, pisz „wykryto na podstawie metadanych stacji”. Nazwy stacji to tekst; Tuneping nie dostarcza logo ani grafik. Zob. warunki.