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.

Publiczna betaEndpointy na żywo (wyszukiwanie, zbiorcze „teraz gra”, „teraz gra” dla stacji, katalog stacji, stan i statystyki) działają bez klucza, z limitem na adres IP. Klucze są samoobsługowe na stronie rejestracji: Free od razu, Developer i Pro po Stripe Checkout. Business: napisz na [email protected].

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

PlanTempoPrzydziałId w żądaniu zbiorczymHistoria
Anonimowo (na IP)2 req/s, seria 51000 / dzień20brak
Free2 req/s, seria 51000 / dzień20brak
Developer10 req/s, seria 20100 000 / miesiąc1007 dni
Pro50 req/s, seria 1001 000 000 / miesiąc10012 miesięcy
Businessindywidualnieindywidualnie10012 miesięcy
Tapelark (token uprawnień, na licencję)5 req/s, seria 102000 / dzień507 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:

HTTPcodeKiedy
400invalid_requestNieznany parametr zapytania lub niepoprawne żądanie
400invalid_cursorKursor niepoprawny, wygasły (24 h; 30 dni dla since_cursor kanału dopasowań) lub użyty z innymi filtrami
401unauthenticated · invalid_api_key · key_revokedBrak klucza, klucz nieznany lub unieważniony
403forbidden_tierEndpoint lub parametr wymaga wyższego planu; required_tier podaje którego
403forbidden_scopeKlucz wydano bez tego zakresu; required_scope podaje którego
404not_found · station_opted_outNieznane id albo stacja poprosiła, by jej nie indeksować
422validation_failedBłędy pól w errors[]
429rate_limited · quota_exceededZwolnij; Retry-After w sekundach
503unavailableBaza 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

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.

ParametrTypDomyślnieZnaczenie
qstring, 2–120 znakówwymaganyWykonawca, tytuł lub oba. Od trzech znaków używane jest dopasowanie trigramowe; krótsze zapytania to dopasowanie podciągu.
countryISO 3166-1 alpha-2, powtarzalnydowolnyTylko stacje z tych krajów.
codecstringdowolnynp. MP3, AAC.
min_bitrateinteger0kbit/s.
classificationenumsongsong, ident, programme, ad_break, empty, junk, unknown lub any.
max_age_seconds30–900180Tylko wiersze, których stacja została pomyślnie odpytana w ciągu tylu sekund.
limit1–10020Liczba 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_seconds w 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.

ParametrTypDomyślnieZnaczenie
station_idsUUID rozdzielone przecinkamiwymaganyDo 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_seconds30–86400900Starsze 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ę.

ParametrTypDomyślnieZnaczenie
qstring—Nazwa stacji, rozmyte.
countryalpha-2, powtarzalnydowolny
tagstring, powtarzalnydowolnyMuszą pasować wszystkie podane tagi.
codec, min_bitrateJak w wyszukiwaniu.
has_now_playingbooleanfalseTylko stacje, z których Tuneping obecnie czyta tytuły.
sortenumclickcountclickcount (popularność, malejąco), name, bitrate.
limit1–20050
cursornieprzezroczysty 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.

ParametrTypDomyślnieZnaczenie
from, toRFC 3339to = teraz, from = to − 24 hNajwyżej 31 dni na wywołanie.
classificationenumsongJak w wyszukiwaniu.
limit1–20050
cursornieprzezroczysty 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.

PoleTypZnaczenie
kindartist · track · queryartist 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.
valuestring, ≤ 240Id lub tekst.
matchexact · fuzzyTylko query. Exact porównuje znormalizowany tekst; fuzzy akceptuje podobieństwo trigramowe 0,6 lub wyższe.
station_filterobjectstation_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.
deliveryarraypoll (domyślnie), sse, webhook. Dostarczanie webhookiem jest dostępne od planu Pro i wymaga webhook_id oraz klucza z zakresem webhooks.
descriptionstring, ≤ 200Twó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.

ZdarzenieKiedyWymagane w Pro
wishlist.matchedPasuje lista życzeń, która wskazuje ten endpoint w webhook_id.—
station.title_changedZmienił się wiersz tytułu stacji.filters.station_ids (≤ 200)
track.startedZaczął się połączony utwór.filters.track_ids lub artist_ids (≤ 200)
artist.startedZaczął 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
ParametrDomyślnieZnaczenie
station_ids—Id stacji rozdzielone przecinkami, najwyżej 100.
wishlist_id—all lub id listy życzeń. Wysyła wishlist.matched.
eventsstation.title_changedDla strumieni stacji dowolne z station.title_changed, track.started, artist.started.
classificationsongsong 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_…, wykonawcy art_…, zdarzenia odtworzeń pe_…, klucze key_…, właściciele own_…, listy życzeń wl_…, dopasowania wm_…, webhooki wh_…, dostarczenia dl_…, zdarzenia evt_…. 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.