Tuneping API v1
Eine JSON-API dafür, was Internetradiosender gerade spielen und was sie gespielt haben. Basis-URL https://api.tuneping.com/v1. Nur Metadaten: der Dienst liest die öffentliche Titelzeile jedes Streams und rührt das Audio nie an.
Schnellstart
Wer spielt gerade Madonna?
curl "https://api.tuneping.com/v1/now-playing/search?q=madonna&limit=3"
Was gerade auf einem Sender läuft, dessen ID Sie kennen (IDs sind die Sender-UUIDs von Radio Browser):
curl "https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/now-playing"
Was ein Sender in den letzten 24 Stunden gespielt hat (erfordert einen Schlüssel mit Verlauf, ab dem Developer-Tarif):
curl -H "Authorization: Bearer tp_live_…" \
"https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/history"
Jede Antwort ist JSON. Fehler sind application/problem+json (siehe Fehler). Unbekannte Query-Parameter werden mit 400 abgelehnt statt ignoriert, sodass Tippfehler sofort auffallen.
Einen Schlüssel erhalten
Selbstbedienung auf tuneping.com/signup: E-Mail-Adresse eingeben, den zugesandten Link innerhalb von 60 Minuten öffnen, und der Schlüssel wird einmal angezeigt. Dieselben zwei Schritte gibt es als API: POST /v1/signup mit {"email": "…", "plan": "free|developer|pro"} antwortet mit 202 und sendet die E-Mail; POST /v1/signup/verify mit dem {"token": "…"} aus dem Link antwortet mit 201 und dem Schlüssel. Free braucht sonst nichts. Developer und Pro gehen über den Link in der Antwort weiter zu Stripe Checkout; der Tarif des Inhabers ändert sich innerhalb einer Minute nach der Zahlung, und alle Schlüssel des Inhabers folgen.
Schlüssel verloren? Registrieren Sie sich erneut mit derselben Adresse: Der Schlüssel wird rotiert, der alte funktioniert noch 24 Stunden. Ein Live-Schlüssel bei Free und Developer, fünf bei Pro, zwanzig bei Business; dazu ein tp_test_-Schlüssel in jedem Tarif, auf Anfrage ausgestellt, bis das Portal verfügbar ist.
Authentifizierung
Senden Sie Ihren Schlüssel als Bearer-Token oder in X-Api-Key:
Authorization: Bearer tp_live_a1b2c3…
X-Api-Key: tp_live_a1b2c3…
Schlüssel beginnen mit tp_live_ oder tp_test_, gefolgt von 43 Zeichen. Eine Anfrage ohne Schlüssel gilt als anonym und wird pro IP-Adresse begrenzt. Ein fehlerhafter, unbekannter oder widerrufener Schlüssel führt sofort zu 401, auch bei Endpoints, die anonym funktioniert hätten. Endpoints, die unten mit Schlüssel markiert sind, antworten ohne Schlüssel mit 401 unauthenticated.
Scopes. Schlüssel haben alle Scopes (read, exports, webhooks, wishlists), sofern sie nicht mit einer engeren Liste ausgestellt wurden, zum Beispiel nur lesend; GET /v1/me listet die Scopes eines Schlüssels auf. Wunschlisten und Webhooks benötigen die Scopes wishlists und webhooks (eine Wunschliste mit Zustellung per webhook braucht beide, geprüft vor dem Tarif). Ein Schlüssel ohne den Scope antwortet mit 403 forbidden_scope samt required_scope, und WWW-Authenticate enthält error="insufficient_scope".
Testschlüssel. Ein tp_test_-Schlüssel liest dieselben Live-Daten mit den Limits von Free und hat eigene Wunschlisten, Treffer und Webhook-Endpoints (livemode: false). Er sieht und ändert die Live-Objekte nie, sodass eine Testsuite eine leere Menge von Wunschlisten per PUT setzen kann, ohne die Produktion zu berühren. Webhooks im Testmodus dürfen nur auf webhook.site zeigen.
Wiederholungen. POST, PATCH und DELETE auf Endpoints, die einen Schlüssel oder ein Partner-Token benötigen, akzeptieren einen Idempotency-Key-Header (eine UUID pro logischer Anfrage); die Registrierungs-Endpoints nehmen ihn nicht an. Eine Wiederholung mit demselben Idempotency-Key und derselben Anfrage erhält erneut die erste erfolgreiche Antwort, markiert mit Idempotent-Replayed: true, statt eine zweite Wunschliste oder einen zweiten Endpoint anzulegen. Der Idempotency-Key wird 24 Stunden lang gespeichert; wird er für eine andere Anfrage wiederverwendet, folgt 422 idempotency_key_reused, und eine Wiederholung, während die erste Anfrage noch läuft, ergibt 409 idempotency_in_progress. Fehler werden nicht gespeichert, eine Wiederholung nach einem Fehler wird also erneut ausgeführt. Eine abgebrochene Verbindung bricht eine Anfrage mit diesem Header nicht ab: Sie wird zu Ende ausgeführt, und eine erfolgreiche Antwort wird gespeichert, sodass Ihre Wiederholung sie erhält. Eine Anfrage, die nach 20 Sekunden noch läuft, wird abgebrochen, ohne dass etwas geändert wurde, und antwortet mit 503 unavailable und Retry-After: 1; wiederholen Sie sie mit demselben Idempotency-Key.
Partner-Apps (derzeit Tapelark) senden statt eines Schlüssels ein signiertes Entitlement-Token: Authorization: Bearer TPE1-…. Das Token wird offline gegen die Ed25519-Schlüssel des Partners verifiziert und auf den Partnertarif in der Tabelle unten abgebildet; die API erfährt nie, wer der Endnutzer ist. Ein fehlerhaftes oder unbekanntes Token antwortet mit 401 token_invalid, ein abgelaufenes mit 401 token_expired (erneuern Sie es), ein Token für eine andere Audience mit 401 token_audience und ein abgelaufenes oder widerrufenes Entitlement mit 403 entitlement_revoked.
Rate-Limits und Kontingente
| Tarif | Rate | Kontingent | Batch-IDs | Verlauf |
|---|---|---|---|---|
| Anonym (pro IP) | 2 req/s, Burst 5 | 1.000 / Tag | 20 | keiner |
| Free | 2 req/s, Burst 5 | 1.000 / Tag | 20 | keiner |
| Developer | 10 req/s, Burst 20 | 100.000 / Monat | 100 | 7 Tage |
| Pro | 50 req/s, Burst 100 | 1.000.000 / Monat | 100 | 12 Monate |
| Business | individuell | individuell | 100 | 12 Monate |
| Tapelark (Entitlement-Token, pro Lizenz) | 5 req/s, Burst 10 | 2.000 / Tag | 50 | 7 Tage |
Das Limit pro Sekunde ist ein Token-Bucket. Ist er leer, erhalten Sie 429 rate_limited mit Retry-After. Ist das Kontingent aufgebraucht, erhalten Sie 429 quota_exceeded, wobei Retry-After die Sekunden bis zum Zurücksetzen des Zeitfensters angibt.
Authentifizierte Antworten, die nicht in gemeinsamen Caches gespeichert werden dürfen, enthalten die Rate-Limit-Felder der IETF:
RateLimit-Policy: "burst";q=10;w=1, "monthly";q=100000;w=2592000
RateLimit: "burst";r=7;t=1
Antworten, die in gemeinsamen Caches gespeichert werden dürfen (Suche, Batch, Jetzt läuft pro Sender, Sender, Statistiken, Health-Zeitreihe), enthalten keine Rate-Limit-Header, weil die Edge denselben Body auch an andere Aufrufer ausliefern kann. Ihr verbleibendes Budget sehen Sie jederzeit unter GET /v1/me.
Caching
Live-Antworten sind 5 Sekunden lang cachebar (Cache-Control: public, max-age=5, s-maxage=5) und tragen ein schwaches ETag. Senden Sie If-None-Match, um 304 zu erhalten, wenn sich nichts geändert hat; ein 304 wird am Origin nicht auf Ihr Kontingent angerechnet. Das Senderverzeichnis wird 60 Sekunden gecacht, ein einzelner Sender 300. Der Verlauf ist private, max-age=30. Health- und Schlüssel-Endpoints sind no-store.
Jede Antwort enthält X-Request-Id. Nennen Sie sie, wenn Sie dem Support schreiben.
Fehler
Fehler folgen den Problem Details nach RFC 9457 mit zwei Erweiterungsfeldern: code, einer stabilen, maschinenlesbaren Zeichenkette, und endpoint-spezifischen Zusätzen wie errors[] bei Validierungsfehlern oder required_tier bei 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." } ]
}
Die vollständige Liste der Codes mit ihrer Bedeutung steht im Fehlerkatalog. Diese begegnen Ihnen zuerst:
| HTTP | Code | Wann |
|---|---|---|
| 400 | invalid_request | Unbekannter Query-Parameter oder fehlerhafte Anfrage |
| 400 | invalid_cursor | Cursor fehlerhaft, abgelaufen (24 h; 30 Tage für einen since_cursor des Treffer-Feeds) oder mit anderen Filtern verwendet |
| 401 | unauthenticated · invalid_api_key · key_revoked | Schlüssel fehlt, ist unbekannt oder widerrufen |
| 403 | forbidden_tier | Endpoint oder Parameter erfordert einen höheren Tarif; required_tier nennt ihn |
| 403 | forbidden_scope | Der Schlüssel wurde ohne den Scope ausgestellt; required_scope nennt ihn |
| 404 | not_found · station_opted_out | Unbekannte ID, oder der Sender hat gebeten, nicht indexiert zu werden |
| 422 | validation_failed | Feldfehler in errors[] |
| 429 | rate_limited · quota_exceeded | Langsamer werden; Retry-After in Sekunden |
| 503 | unavailable | Datenbank nicht verfügbar, oder eine Anfrage mit Idempotency-Key nach 20 Sekunden abgebrochen; nach Retry-After wiederholen |
GET/v1/now-playing/search ohne Schlüssel public, max-age=5 · ETag
Live-Suche
Welche Sender gerade q spielen. Unscharf und unabhängig von Akzenten: gronemeyer manner findet Grönemeyer – Männer. Die Zeilen sind nach Ähnlichkeit sortiert, dann danach, wie kürzlich der Titel erschienen ist. Keine Paginierung: Der Index ändert sich alle paar Sekunden, Seiten würden also Einträge überspringen oder wiederholen. Erhöhen Sie stattdessen limit.
| Parameter | Typ | Standard | Bedeutung |
|---|---|---|---|
q | String, 2–120 Zeichen | erforderlich | Interpret, Titel oder beides. Ab drei Zeichen wird per Trigramm verglichen; kürzere Anfragen sind eine Teilstring-Suche. |
country | ISO 3166-1 alpha-2, wiederholbar | alle | Nur Sender aus diesen Ländern. |
codec | String | alle | z. B. MP3, AAC. |
min_bitrate | Integer | 0 | kbit/s. |
classification | Enum | song | song, ident, programme, ad_break, empty, junk, unknown oder any. |
max_age_seconds | 30–900 | 180 | Nur Zeilen, deren Sender innerhalb so vieler Sekunden erfolgreich abgefragt wurde. |
limit | 1–100 | 20 | Anzahl der zurückgegebenen Zeilen; truncated: true bedeutet, dass es mehr gab. |
Antwort · 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 }
}
Eine Zeile lesen
- started_at RFC 3339 · Schätzung
- Geschätzter Beginn des aktuellen Titels, ±
start_uncertainty_secondsin beide Richtungen. - observed_at RFC 3339
- Wann Tuneping diesen Titel zuerst gesehen hat.
- confirmed_at RFC 3339
- Die letzte erfolgreiche Abfrage des Senders: „läuft noch, Stand“. Verwenden Sie es zur Beurteilung der Aktualität.
- track Objekt · optional
- Vorhanden, wenn der Titel mit einer MusicBrainz-Aufnahme verknüpft wurde.
- similarity 0–1
- Schneiden Sie bei Ihrem eigenen Schwellenwert ab.
GET/v1/now-playing ohne Schlüssel public, max-age=5 · ETag
Jetzt läuft im Batch
Der aktuelle Titel für eine Liste von Sendern in einem Aufruf, für Favoriten-Ansichten.
| Parameter | Typ | Standard | Bedeutung |
|---|---|---|---|
station_ids | kommagetrennte UUIDs | erforderlich | Bis zu 20 anonym oder bei Free, 100 ab Developer. IDs werden vor dem Caching sortiert und dedupliziert, sodass sich zwei Clients mit denselben Favoriten einen gecachten Body teilen. |
max_age_seconds | 30–86400 | 900 | Ältere Zeilen werden weiterhin zurückgegeben, aber mit is_stale: true markiert. |
{ "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 ohne Schlüssel
Jetzt läuft pro Sender
Eine Zeile für einen Sender. 404 not_found, wenn noch kein Titel beobachtet wurde, 404 station_opted_out, wenn der Sender gebeten hat, nicht indexiert zu werden.
GET/v1/stations GET/v1/stations/{id} ohne Schlüssel max-age=60 · 300
Sender
Das Verzeichnis, aufgebaut aus dem von der Community gepflegten Radio-Browser-Verzeichnis und ergänzt um die Angabe, wie Tuneping jeden Sender liest.
| Parameter | Typ | Standard | Bedeutung |
|---|---|---|---|
q | String | — | Sendername, unscharf. |
country | alpha-2, wiederholbar | alle | |
tag | String, wiederholbar | alle | Alle angegebenen Tags müssen passen. |
codec, min_bitrate | Wie bei der Suche. | ||
has_now_playing | Boolean | false | Nur Sender, von denen Tuneping derzeit Titel liest. |
sort | Enum | clickcount | clickcount (Beliebtheit, absteigend), name, bitrate. |
limit | 1–200 | 50 | |
cursor | opaker String | — | Aus next_cursor der vorherigen Seite; 24 Stunden gültig und nur mit denselben Filtern. |
{ "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 wird als Text aus dem Verzeichnis durchgereicht; Tuneping kopiert oder liefert niemals Logos aus.
GET/v1/stations/{id}/history Schlüssel · ab Developer private, max-age=30
Wiedergabeverlauf
Songs, die ein Sender gespielt hat, neueste zuerst, mit geschätztem Beginn und Ende. Wie weit der Verlauf zurückreicht, hängt vom Tarif ab: 7 Tage bei Developer, 12 Monate bei Pro und Business. Ein älteres from führt zu 403 forbidden_tier mit required_tier, nie zu einem stillschweigend gekürzten Zeitfenster.
| Parameter | Typ | Standard | Bedeutung |
|---|---|---|---|
from, to | RFC 3339 | to = jetzt, from = to − 24 h | Höchstens 31 Tage pro Aufruf. |
classification | Enum | song | Wie bei der Suche. |
limit | 1–200 | 50 | |
cursor | opaker String | — | Das Zeitfenster ist im Cursor festgeschrieben. |
{ "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 Schlüssel no-store
Schlüssel und Nutzung
/v1/me liefert den Schlüssel, Tarif und Status seines Inhabers, die geltenden Limits und die bisherige Nutzung im aktuellen Zeitfenster. /v1/me/usage?from=&to= liefert die Anfragen pro Tag (nur Anfragen am Origin; Treffer im Edge-Cache werden nicht gezählt).
{ "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 ohne Schlüssel
Health und Statistiken
/v1/health antwortet mit 200 und status ok oder degraded, mit 503, wenn die Datenbank ausgefallen ist; richten Sie Ihr Uptime-Monitoring darauf. /v1/health/series?minutes=60 (5–1440) liefert Kennzahlen des Abfragedienstes pro Minute: polls, ok, titles_changed, songs, bytes_in, bytes_out, latency_p50_ms, latency_p90_ms, inflight_max, ips_paused, queue_lag_seconds und eine Aufschlüsselung by_method. /v1/stats ist der öffentliche Abdeckungs-Snapshot, den die Statusseite zeigt: Sender insgesamt, abgefragt, mit Live-Metadaten, Titelwechsel pro Minute, Anteil der Songs, nach Methode, nach Land.
POST/v1/wishlists PUT/v1/wishlists GET/v1/wishlists/matches Schlüssel · ab Developer no-store
Wunschlisten
Nennen Sie einen Song, einen Interpreten oder einen Freitext-Titel und erfahren Sie, wann ein Sender ihn zu spielen beginnt. Jeder Titelwechsel wird beim Lesen mit Ihren Wunschlisten abgeglichen, und eine neue Wunschliste ist innerhalb von 30 Sekunden aktiv. Developer speichert 10 Wunschlisten, Pro 100, Business 1.000. Treffer werden 30 Tage aufbewahrt.
| Feld | Typ | Bedeutung |
|---|---|---|
kind | artist · track · query | artist und track nehmen eine ID aus der Suche (art_…, trk_…) und erfassen auch Schreibweisen, die der Normalisierer damit verknüpft. query nimmt Freitext, normalerweise Artist - Title. |
value | String, ≤ 240 | Die ID oder der Text. |
match | exact · fuzzy | Nur bei query. Exact vergleicht den normalisierten Text; fuzzy akzeptiert eine Trigramm-Ähnlichkeit von 0,6 oder mehr. |
station_filter | Objekt | station_ids (≤ 500) und/oder countries (alpha-2 in beliebiger Schreibung, zurückgegeben in Großbuchstaben). Ein Titel passt, wenn sein Sender aufgeführt ist oder in einem aufgeführten Land liegt. Ohne Filter gilt jeder Sender. |
delivery | Array | poll (Standard), sse, webhook. Zustellung per Webhook gibt es ab Pro; sie benötigt webhook_id und einen Schlüssel mit dem Scope webhooks. |
description | String, ≤ 200 | Für Sie. |
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
Eine ausdrückliche station_ids-Liste lässt Tuneping diese Sender alle 30 statt alle 60 Sekunden lesen, solange die Wunschliste aktiviert ist, und die Antwort sagt das mit tightens: true. Die Vereinigungsmenge Ihrer aufgeführten Sender ist pro Tarif begrenzt (limits.tightened_stations in /v1/me); eine Wunschliste, die diese Grenze überschreiten würde, antwortet mit 422 validation_failed und einem Eintrag in errors[], dessen code tightened_stations_exceeded ist (Feld station_filter.station_ids, bei PUT data). Länderfilter greifen weiterhin, im normalen Intervall des jeweiligen Senders.
GET /v1/wishlists/{id}, PATCH (Senderfilter, match, delivery, Webhook, Beschreibung, status: enabled | paused) und DELETE funktionieren wie erwartet. PUT /v1/wishlists mit {"data": [ … ]} ersetzt Ihre gesamte Menge in einem Aufruf: Unveränderte Einträge behalten ihre ID und ihren Trefferzähler, die übrigen werden angelegt oder gelöscht. So lässt sich eine Liste, die Ihre App lokal führt, am einfachsten synchronisieren. Der Body darf bis zu 1 MiB groß sein; andere Request-Bodys sind auf 64 KiB begrenzt (413 payload_too_large).
Treffer sind ein fortlaufender Feed. GET /v1/wishlists/matches umfasst alle Ihre Wunschlisten, GET /v1/wishlists/{id}/matches eine davon. Die Zeilen kommen älteste zuerst. Behalten Sie den next_cursor jeder Antwort und senden Sie ihn als since_cursor zurück: Er ist nie null, bleibt für die Aufbewahrungsdauer von 30 Tagen gültig und übersteht Neustarts. Ohne Cursor erhalten Sie die neuesten Treffer. limit ist 1–500, Standard 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 ist bereits um den Zeitpunkt korrigiert, zu dem der Titel gesehen wurde, sodass ein Recorder „begann vor etwa N ± U Sekunden“ anzeigen kann. expected_end_hint_at ist started_at plus 222 Sekunden, der Median der Songlängen: ein Hinweis, keine Vorhersage. confidence ist high, wenn der Treffer über eine verknüpfte Track- oder Interpreten-ID zustande kam, medium bei normalisiertem Interpret und Titel, low bei Textanfragen.
POST/v1/webhooks GET/v1/webhooks/{id}/deliveries Schlüssel · ab Pro no-store
Webhooks
Tuneping sendet Events per POST an Ihren HTTPS-Endpoint, sobald sie eintreten. Pro hat 5 Endpoints, Business 20.
| Event | Wann | Pro benötigt |
|---|---|---|
wishlist.matched | Eine Wunschliste, die diesen Endpoint in webhook_id nennt, hat einen Treffer. | — |
station.title_changed | Die Titelzeile eines Senders hat sich geändert. | filters.station_ids (≤ 200) |
track.started | Ein verknüpfter Track hat begonnen. | filters.track_ids oder artist_ids (≤ 200) |
artist.started | Ein verknüpfter Interpret hat begonnen. | filters.track_ids oder artist_ids (≤ 200) |
Filter können auch countries und classification enthalten. Reine Länderfilter und ungefilterte Streams von Titelwechseln gibt es nur bei Business. Jeder Endpoint hat ein Tagesbudget von 50.000 Events bei Pro und 500.000 bei Business; darüber hinaus ist der Endpoint bis zum nächsten UTC-Tag paused_budget, und Sie erhalten eine 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
Die Antwort enthält das Signatur-secret (whsec_…) ein einziges Mal. Zustellungen folgen Standard Webhooks: Header webhook-id, webhook-timestamp und webhook-signature: v1,<base64>, wobei die Signatur ein HMAC-SHA256 mit den Bytes des Secrets über {webhook-id}.{webhook-timestamp}.{body} ist. Lehnen Sie Zeitstempel ab, die mehr als 5 Minuten abweichen, und verwenden Sie webhook-id, um Duplikate zu verwerfen: Sie bleibt über Wiederholungen hinweg gleich.
{ "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_…" } }
Antworten Sie innerhalb von 10 Sekunden mit einem beliebigen 2xx. Weiterleitungen werden nicht verfolgt. Fehlgeschlagene Zustellungen werden nach 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h und 24 h wiederholt, insgesamt etwa drei Tage, pro Endpoint jeweils eine nach der anderen. Ein Endpoint, der mit 410 antwortet oder an fünf Tagen in Folge fehlschlägt, wird deaktiviert, und Sie erhalten eine E-Mail; setzen Sie status per PATCH wieder auf enabled, sobald er repariert ist.
POST /v1/webhooks/{id}/rotate-secret liefert ein neues Secret; 24 Stunden lang trägt jede Zustellung zwei Signaturen, eine pro Secret. POST /v1/webhooks/{id}/test sendet einen ping. GET /v1/webhooks/{id}/deliveries?status=pending|succeeded|failed listet die Zustellungen der letzten 30 Tage auf, und POST …/deliveries/{dl_id}/redeliver sendet eine davon erneut. Testschlüssel (tp_test_…) dürfen nur auf webhook.site zeigen.
GET/v1/stream Schlüssel · ab Developer text/event-stream
Event-Stream
Dieselben Events über eine einzige lange HTTP-Antwort im Format Server-Sent Events, für Server und native Apps. Übergeben Sie entweder station_ids (bis zu 100) oder wishlist_id (all oder eine wl_…-ID).
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
| Parameter | Standard | Bedeutung |
|---|---|---|
station_ids | — | Kommagetrennte Sender-IDs, höchstens 100. |
wishlist_id | — | all oder eine Wunschlisten-ID. Sendet wishlist.matched. |
events | station.title_changed | Für Sender-Streams beliebige von station.title_changed, track.started, artist.started. |
classification | song | song oder any, für Sender-Streams. |
data ist dasselbe JSON, das ein Webhook erhält. Ein : ping-Kommentar kommt nach 20 Sekunden ohne Event, setzen Sie den Idle-Timeout Ihres Clients also auf 60 Sekunden oder mehr. Bricht die Verbindung ab, verbinden Sie sich erneut mit dem Header Last-Event-ID, gesetzt auf die letzte id, die Sie gesehen haben: Events der letzten 10 Minuten werden erneut gesendet. Eine ältere Position erhält ein event: gap mit der frühesten verfügbaren ID; füllen Sie die Lücke aus dem Verlauf oder dem Treffer-Feed. Offene Streams pro Schlüssel: Developer 1, Pro 5, Business 20, Tapelark 1 pro Gerät. Ein weiterer antwortet mit 429 too_many_connections.
Browser können hier EventSource nicht verwenden, weil es den Authorization-Header nicht senden kann und Schlüssel nie in der URL akzeptiert werden. Lesen Sie den Stream stattdessen mit fetch().
Felder und Enums
- classification Enum
song·ident(Sendername oder Slogan) ·programme·ad_break·empty·junk·unknown. Nur Songs gelangen in den Wiedergabeverlauf; in der Live-Zeile bleibt alles erhalten.- match_grade Enum
high·medium·none. Wie gut der Titel mit einer MusicBrainz-Aufnahme verknüpft wurde.- confidence Enum
high·medium·low. Pro Zeile berechnet aus Klassifizierung, Match-Grad, wie kürzlich der Sender bestätigt wurde und der bisherigen Zuverlässigkeit des Senders. Dokumentiert, damit Sie es nachprüfen können.- method Enum
- Wie Tuneping den Sender liest:
icy(Metadaten im Stream),icecast_json(Server-Status als JSON),shoutcast_stats(Status als XML),shoutcast_currentsong,seven_html. Interne Bezeichner, keine Empfehlungen; zeigen Sie sie, wenn überhaupt, als „im Stream“ und „Statusseite“ an. - recordability Enum
- Ob der Stream des Senders aus Sicht eines aufzeichnenden Clients ein einfacher, aufzeichenbarer HTTP-Stream ist. Nur zur Information.
- IDs
- Sender verwenden ihre Radio-Browser-UUID. Tracks sind
trk_…, Interpretenart_…, Wiedergabe-Eventspe_…, Schlüsselkey_…, Inhaberown_…, Wunschlistenwl_…, Trefferwm_…, Webhookswh_…, Zustellungendl_…, Eventsevt_…. Behandeln Sie alle als opake Strings. - Zeitstempel
- RFC 3339, UTC, mit Millisekunden-Genauigkeit, wo die Quelle sie liefert.
OpenAPI
Die maschinenlesbare Beschreibung dieser API liegt unter /docs/openapi-v1.yaml (OpenAPI 3.1). Sie ist die Grundlage für die Client-Generierung; die Operation-IDs sind stabil.
Namensnennung und faire Nutzung
Bei Nutzung im Free-Tarif und anonymer Nutzung wird dort, wo die Daten angezeigt werden, ein sichtbarer Link „Data by Tuneping“ angezeigt (Badge). Wiedergaben werden anhand von Sender-Metadaten erkannt und sind kein zertifiziertes Monitoring; schreiben Sie „anhand von Sender-Metadaten erkannt“, wo Sie sie als Nachweis vorlegen. Sendernamen sind Text; Tuneping stellt keine Logos oder Artwork bereit. Siehe die Nutzungsbedingungen.