Tuneping API v1
Một JSON API cho biết các đài radio internet đang phát gì ngay lúc này, và đã phát gì. Base URL https://api.tuneping.com/v1. Chỉ metadata: dịch vụ đọc dòng tiêu đề công khai của mỗi luồng và không bao giờ chạm vào âm thanh.
Bắt đầu nhanh
Ai đang phát Madonna ngay lúc này?
curl "https://api.tuneping.com/v1/now-playing/search?q=madonna&limit=3"
Bài đang phát trên một đài mà bạn biết id (id là UUID đài của Radio Browser):
curl "https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/now-playing"
Một đài đã phát gì trong 24 giờ qua (cần khóa có quyền xem lịch sử, gói Developer trở lên):
curl -H "Authorization: Bearer tp_live_…" \
"https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/history"
Mọi phản hồi đều là JSON. Lỗi có dạng application/problem+json (xem Lỗi). Query parameter không xác định bị từ chối với 400 thay vì bị bỏ qua, nên lỗi gõ sai lộ ra ngay.
Lấy khóa
Tự phục vụ tại tuneping.com/signup: nhập địa chỉ e-mail, mở liên kết được gửi tới trong vòng 60 phút, và khóa được hiển thị một lần. Hai bước này cũng là một API: POST /v1/signup với {"email": "…", "plan": "free|developer|pro"} trả về 202 và gửi thư; POST /v1/signup/verify với {"token": "…"} lấy từ liên kết trả về 201 kèm khóa. Gói Free không cần gì thêm. Developer và Pro chuyển tiếp sang Stripe Checkout từ liên kết trong phản hồi; gói của chủ sở hữu thay đổi trong vòng một phút sau khi thanh toán và mọi khóa của chủ sở hữu đó đều theo.
Mất khóa? Đăng ký lại bằng cùng địa chỉ: khóa được xoay vòng, khóa cũ vẫn hoạt động thêm 24 giờ. Một khóa live ở gói Free và Developer, năm ở Pro, hai mươi ở Business; cộng thêm một khóa tp_test_ ở mọi gói, cấp theo yêu cầu cho đến khi cổng quản lý ra mắt.
Xác thực
Gửi khóa của bạn dưới dạng bearer token, hoặc trong X-Api-Key:
Authorization: Bearer tp_live_a1b2c3…
X-Api-Key: tp_live_a1b2c3…
Khóa bắt đầu bằng tp_live_ hoặc tp_test_, theo sau là 43 ký tự. Yêu cầu không có khóa được coi là ẩn danh và bị giới hạn theo địa chỉ IP. Khóa sai định dạng, không xác định hoặc đã bị thu hồi sẽ nhận 401 ngay lập tức, kể cả trên các endpoint vốn hoạt động được ở chế độ ẩn danh. Các endpoint được đánh dấu khóa bên dưới trả về 401 unauthenticated nếu không có khóa.
Phạm vi (scope). Khóa mang mọi phạm vi (read, exports, webhooks, wishlists) trừ khi được cấp với danh sách hẹp hơn, ví dụ chỉ đọc; GET /v1/me liệt kê các phạm vi của khóa. Danh sách mong muốn và webhook cần phạm vi wishlists và webhooks (một danh sách mong muốn gửi qua webhook cần cả hai, được kiểm tra trước gói). Khóa không có phạm vi cần thiết trả về 403 forbidden_scope kèm required_scope, và WWW-Authenticate mang error="insufficient_scope".
Khóa thử nghiệm. Khóa tp_test_ đọc cùng dữ liệu live với giới hạn của gói Free, và có danh sách mong muốn, kết quả khớp và endpoint webhook riêng (livemode: false). Nó không bao giờ thấy hay thay đổi những thứ của chế độ live, nên một bộ test có thể PUT một tập danh sách mong muốn rỗng mà không động tới production. Webhook ở chế độ thử nghiệm chỉ được trỏ tới webhook.site.
Thử lại. POST, PATCH và DELETE trên các endpoint cần khóa hoặc token đối tác chấp nhận header Idempotency-Key (một UUID cho mỗi yêu cầu logic); các endpoint đăng ký không nhận header này. Một lần thử lại với cùng Idempotency-Key và cùng yêu cầu sẽ nhận lại phản hồi thành công đầu tiên, được đánh dấu Idempotent-Replayed: true, thay vì tạo ra danh sách mong muốn hay endpoint thứ hai. Idempotency-Key được ghi nhớ trong 24 giờ; dùng lại nó cho một yêu cầu khác sẽ nhận 422 idempotency_key_reused, và thử lại trong khi yêu cầu đầu tiên vẫn đang chạy sẽ nhận 409 idempotency_in_progress. Lỗi không được ghi nhớ, nên thử lại sau một lỗi sẽ chạy lại yêu cầu. Kết nối bị ngắt không hủy yêu cầu có mang header này: yêu cầu vẫn chạy xong và phản hồi thành công được ghi nhớ, nên lần thử lại của bạn sẽ nhận được nó. Yêu cầu vẫn còn chạy sau 20 giây sẽ bị hủy mà không thay đổi gì và trả về 503 unavailable kèm Retry-After: 1; hãy thử lại với cùng Idempotency-Key.
Ứng dụng đối tác (hiện là Tapelark) gửi một entitlement token đã ký thay cho khóa: Authorization: Bearer TPE1-…. Token được xác minh offline bằng khóa Ed25519 của đối tác và tương ứng với gói đối tác trong bảng bên dưới; API không bao giờ biết người dùng cuối là ai. Token sai định dạng hoặc không xác định trả về 401 token_invalid, token hết hạn trả về 401 token_expired (hãy làm mới), token dành cho audience khác trả về 401 token_audience, và quyền sử dụng đã hết hiệu lực hoặc bị thu hồi trả về 403 entitlement_revoked.
Giới hạn tốc độ và hạn mức
| Gói | Tốc độ | Hạn mức | Id theo lô | Lịch sử |
|---|---|---|---|---|
| Ẩn danh (theo IP) | 2 req/s, burst 5 | 1.000 / ngày | 20 | không có |
| Free | 2 req/s, burst 5 | 1.000 / ngày | 20 | không có |
| Developer | 10 req/s, burst 20 | 100.000 / tháng | 100 | 7 ngày |
| Pro | 50 req/s, burst 100 | 1.000.000 / tháng | 100 | 12 tháng |
| Business | tùy chỉnh | tùy chỉnh | 100 | 12 tháng |
| Tapelark (entitlement token, theo giấy phép) | 5 req/s, burst 10 | 2.000 / ngày | 50 | 7 ngày |
Giới hạn theo giây là một token bucket. Khi bucket cạn, bạn nhận 429 rate_limited kèm Retry-After. Khi hạn mức đã dùng hết, bạn nhận 429 quota_exceeded với Retry-After là số giây đến khi cửa sổ được đặt lại.
Các phản hồi đã xác thực mà không thể cache chung sẽ mang các trường giới hạn tốc độ của IETF:
RateLimit-Policy: "burst";q=10;w=1, "monthly";q=100000;w=2592000
RateLimit: "burst";r=7;t=1
Các phản hồi có thể cache chung (tìm kiếm, theo lô, now-playing của đài, danh sách đài, stats, health series) không mang header giới hạn tốc độ, vì edge có thể phục vụ cùng một body cho người gọi khác. Ngân sách còn lại của bạn luôn có tại GET /v1/me.
Cache
Phản hồi trực tiếp có thể cache trong 5 giây (Cache-Control: public, max-age=5, s-maxage=5) và mang một ETag yếu. Gửi If-None-Match để nhận 304 khi không có gì thay đổi; một phản hồi 304 không bị tính vào hạn mức của bạn tại origin. Danh bạ đài được cache 60 giây, một đài đơn lẻ 300 giây. Lịch sử là private, max-age=30. Các endpoint health và khóa là no-store.
Mọi phản hồi đều mang X-Request-Id. Hãy gửi kèm giá trị này khi bạn liên hệ hỗ trợ.
Lỗi
Lỗi tuân theo RFC 9457 problem details với hai thành phần mở rộng: code, một chuỗi ổn định cho máy đọc, và các trường bổ sung riêng theo endpoint như errors[] khi xác thực dữ liệu thất bại hoặc required_tier với 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." } ]
}
Danh sách đầy đủ các mã cùng ý nghĩa nằm trong danh mục lỗi. Những mã bạn sẽ gặp đầu tiên:
| HTTP | code | Khi nào |
|---|---|---|
| 400 | invalid_request | Query parameter không xác định hoặc yêu cầu sai định dạng |
| 400 | invalid_cursor | Cursor sai định dạng, hết hạn (24 h; 30 ngày với since_cursor của feed kết quả khớp) hoặc dùng với bộ lọc khác |
| 401 | unauthenticated · invalid_api_key · key_revoked | Thiếu khóa, khóa không xác định hoặc đã bị thu hồi |
| 403 | forbidden_tier | Endpoint hoặc tham số cần gói cao hơn; required_tier cho biết gói nào |
| 403 | forbidden_scope | Khóa được cấp mà không có phạm vi cần thiết; required_scope cho biết phạm vi nào |
| 404 | not_found · station_opted_out | Id không xác định, hoặc đài đã yêu cầu không được lập chỉ mục |
| 422 | validation_failed | Lỗi theo trường trong errors[] |
| 429 | rate_limited · quota_exceeded | Hãy chậm lại; Retry-After tính bằng giây |
| 503 | unavailable | Cơ sở dữ liệu không khả dụng, hoặc một yêu cầu có Idempotency-Key bị hủy sau 20 giây; thử lại sau Retry-After |
GET/v1/now-playing/search không cần khóa public, max-age=5 · ETag
Tìm kiếm trực tiếp
Những đài nào đang phát q ngay lúc này. Khớp gần đúng và không phân biệt dấu: gronemeyer manner tìm ra Grönemeyer – Männer. Các dòng được sắp xếp theo độ tương đồng, sau đó theo mức độ gần đây mà tiêu đề xuất hiện. Không phân trang: chỉ mục thay đổi sau mỗi vài giây, nên các trang sẽ bị bỏ sót hoặc lặp lại. Thay vào đó, hãy tăng limit.
| Tham số | Kiểu | Mặc định | Ý nghĩa |
|---|---|---|---|
q | chuỗi, 2–120 ký tự | bắt buộc | Nghệ sĩ, tiêu đề hoặc cả hai. Từ ba ký tự trở lên dùng so khớp trigram; truy vấn ngắn hơn là so khớp chuỗi con. |
country | ISO 3166-1 alpha-2, lặp lại được | bất kỳ | Chỉ các đài từ những quốc gia này. |
codec | chuỗi | bất kỳ | ví dụ MP3, AAC. |
min_bitrate | số nguyên | 0 | kbit/s. |
classification | enum | song | song, ident, programme, ad_break, empty, junk, unknown hoặc any. |
max_age_seconds | 30–900 | 180 | Chỉ các dòng có đài được poll thành công trong vòng số giây này. |
limit | 1–100 | 20 | Số dòng trả về; truncated: true cho biết còn nhiều hơn. |
Phản hồi · 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 một dòng
- started_at RFC 3339 · ước tính
- Thời điểm bắt đầu ước tính của tiêu đề hiện tại, sai lệch ±
start_uncertainty_secondsvề cả hai phía. - observed_at RFC 3339
- Thời điểm Tuneping thấy tiêu đề này lần đầu.
- confirmed_at RFC 3339
- Lần poll thành công gần nhất của đài: "vẫn đang phát tính đến lúc". Dùng trường này để đánh giá độ mới.
- track object · tùy chọn
- Có mặt khi tiêu đề được liên kết với một recording của MusicBrainz.
- similarity 0–1
- Tự cắt theo ngưỡng của bạn.
GET/v1/now-playing không cần khóa public, max-age=5 · ETag
Now-playing theo lô
Tiêu đề hiện tại cho một danh sách đài trong một lần gọi, dành cho màn hình đài yêu thích.
| Tham số | Kiểu | Mặc định | Ý nghĩa |
|---|---|---|---|
station_ids | các UUID cách nhau bằng dấu phẩy | bắt buộc | Tối đa 20 khi ẩn danh hoặc Free, 100 từ Developer trở lên. Id được sắp xếp và loại trùng trước khi cache, nên hai client có cùng danh sách yêu thích dùng chung một body đã cache. |
max_age_seconds | 30–86400 | 900 | Các dòng cũ hơn giá trị này vẫn được trả về nhưng được gắn cờ 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 không cần khóa
Now-playing của đài
Một dòng cho một đài. 404 not_found khi chưa quan sát được tiêu đề nào, 404 station_opted_out khi đài đã yêu cầu không được lập chỉ mục.
GET/v1/stations GET/v1/stations/{id} không cần khóa max-age=60 · 300
Đài
Danh bạ, xây dựng từ danh bạ Radio Browser do cộng đồng duy trì và bổ sung thông tin về cách Tuneping đọc từng đài.
| Tham số | Kiểu | Mặc định | Ý nghĩa |
|---|---|---|---|
q | chuỗi | — | Tên đài, khớp gần đúng. |
country | alpha-2, lặp lại được | bất kỳ | |
tag | chuỗi, lặp lại được | bất kỳ | Mọi tag đã cho đều phải khớp. |
codec, min_bitrate | Như trong tìm kiếm. | ||
has_now_playing | boolean | false | Chỉ các đài mà Tuneping hiện đang đọc được tiêu đề. |
sort | enum | clickcount | clickcount (độ phổ biến, giảm dần), name, bitrate. |
limit | 1–200 | 50 | |
cursor | chuỗi mờ (opaque) | — | Lấy từ next_cursor của trang trước; có hiệu lực 24 giờ và chỉ với cùng bộ lọc. |
{ "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 được chuyển tiếp nguyên dạng văn bản từ danh bạ; Tuneping không bao giờ sao chép hay phục vụ logo.
GET/v1/stations/{id}/history khóa · developer + private, max-age=30
Lịch sử phát
Các bài hát một đài đã phát, mới nhất trước, kèm thời điểm bắt đầu và kết thúc ước tính. Độ sâu tùy theo gói: 7 ngày với Developer, 12 tháng với Pro và Business. Yêu cầu from cũ hơn sẽ nhận 403 forbidden_tier kèm required_tier, không bao giờ âm thầm thu hẹp khoảng thời gian.
| Tham số | Kiểu | Mặc định | Ý nghĩa |
|---|---|---|---|
from, to | RFC 3339 | to = hiện tại, from = to − 24 h | Tối đa 31 ngày mỗi lần gọi. |
classification | enum | song | Như trong tìm kiếm. |
limit | 1–200 | 50 | |
cursor | chuỗi mờ (opaque) | — | Khoảng thời gian được cố định trong 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 khóa no-store
Khóa và mức sử dụng
/v1/me trả về khóa, gói và trạng thái của chủ sở hữu khóa, các giới hạn áp dụng và mức sử dụng tính đến hiện tại trong cửa sổ hiện hành. /v1/me/usage?from=&to= trả về số yêu cầu mỗi ngày (chỉ yêu cầu tới origin; lượt trúng cache tại edge không được tính).
{ "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 không cần khóa
Health và stats
/v1/health trả về 200 với status là ok hoặc degraded, và 503 khi cơ sở dữ liệu ngừng hoạt động; hãy trỏ công cụ giám sát uptime của bạn vào đây. /v1/health/series?minutes=60 (5–1440) trả về số liệu của poller theo từng phút: polls, ok, titles_changed, songs, bytes_in, bytes_out, latency_p50_ms, latency_p90_ms, inflight_max, ips_paused, queue_lag_seconds và bảng phân tích by_method. /v1/stats là ảnh chụp độ phủ công khai hiển thị trên trang trạng thái: tổng số đài, số đài được poll, số đài có metadata trực tiếp, số lần đổi tiêu đề mỗi phút, tỷ lệ bài hát, theo phương thức, theo quốc gia.
POST/v1/wishlists PUT/v1/wishlists GET/v1/wishlists/matches khóa · developer + no-store
Danh sách mong muốn
Nêu tên một bài hát, một nghệ sĩ hoặc một tiêu đề dạng văn bản tự do và biết khi nào một đài bắt đầu phát nó. Mỗi lần đổi tiêu đề đều được đối chiếu với các danh sách mong muốn của bạn ngay khi được đọc, và một danh sách mong muốn mới có hiệu lực trong vòng 30 giây. Developer giữ được 10 danh sách mong muốn, Pro 100, Business 1.000. Kết quả khớp được lưu 30 ngày.
| Trường | Kiểu | Ý nghĩa |
|---|---|---|
kind | artist · track · query | artist và track nhận một id từ tìm kiếm (art_…, trk_…) và cũng bắt được các cách viết mà bộ chuẩn hóa liên kết tới id đó. query nhận văn bản tự do, thường là Artist - Title. |
value | chuỗi, ≤ 240 | Id hoặc văn bản. |
match | exact · fuzzy | Chỉ với query. Exact so sánh văn bản đã chuẩn hóa; fuzzy chấp nhận độ tương đồng trigram từ 0,6 trở lên. |
station_filter | object | station_ids (≤ 500) và/hoặc countries (alpha-2 viết hoa hay thường đều được, trả về dạng viết hoa). Một tiêu đề khớp khi đài của nó có trong danh sách hoặc thuộc một quốc gia có trong danh sách. Không có bộ lọc nghĩa là mọi đài. |
delivery | mảng | poll (mặc định), sse, webhook. Gửi qua webhook dành cho Pro trở lên và cần webhook_id cùng một khóa có phạm vi webhooks. |
description | chuỗi, ≤ 200 | Của riêng bạn. |
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
Một danh sách station_ids tường minh khiến Tuneping đọc các đài đó cứ 30 giây một lần thay vì 60 giây trong khi danh sách mong muốn được bật, và phản hồi cho biết điều đó bằng tightens: true. Hợp của tất cả các đài bạn liệt kê bị giới hạn theo gói (limits.tightened_stations trong /v1/me); một danh sách mong muốn vượt quá giới hạn đó sẽ nhận 422 validation_failed với một mục errors[] có code là tightened_stations_exceeded (trường station_filter.station_ids, hoặc data khi dùng PUT). Bộ lọc theo quốc gia vẫn khớp, theo chu kỳ bình thường của từng đài.
GET /v1/wishlists/{id}, PATCH (bộ lọc đài, match, delivery, webhook, description, status: enabled | paused) và DELETE hoạt động như bạn mong đợi. PUT /v1/wishlists với {"data": [ … ]} thay thế toàn bộ tập của bạn trong một lần gọi: các mục không đổi giữ nguyên id và số lần khớp, phần còn lại được tạo mới hoặc xóa. Đây là cách dễ nhất để đồng bộ một danh sách mà ứng dụng của bạn lưu cục bộ. Body của nó có thể lên tới 1 MiB; body của các yêu cầu khác giới hạn ở 64 KiB (413 payload_too_large).
Kết quả khớp là một feed dạng tail. GET /v1/wishlists/matches bao quát mọi danh sách mong muốn của bạn và GET /v1/wishlists/{id}/matches cho một danh sách. Các dòng đến theo thứ tự cũ nhất trước. Hãy giữ next_cursor từ mỗi phản hồi và gửi lại dưới dạng since_cursor: nó không bao giờ null, có hiệu lực trong suốt thời gian lưu giữ 30 ngày và tồn tại qua các lần khởi động lại. Không có cursor, bạn nhận các kết quả khớp mới nhất. limit là 1–500, mặc định 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 đã được hiệu chỉnh theo thời điểm tiêu đề được thấy, nên một ứng dụng ghi âm có thể hiển thị "bắt đầu khoảng N ± U giây trước". expected_end_hint_at là started_at cộng 222 giây, độ dài trung vị của một bài hát: một gợi ý, không phải dự đoán. confidence là high khi kết quả khớp đi qua một id bản nhạc hoặc nghệ sĩ đã liên kết, medium khi khớp theo nghệ sĩ và tiêu đề đã chuẩn hóa, low với truy vấn văn bản.
POST/v1/webhooks GET/v1/webhooks/{id}/deliveries khóa · pro + no-store
Webhook
Tuneping gửi sự kiện tới endpoint HTTPS của bạn ngay khi chúng xảy ra. Pro có 5 endpoint, Business 20.
| Sự kiện | Khi nào | Pro cần |
|---|---|---|
wishlist.matched | Một danh sách mong muốn có nêu endpoint này trong webhook_id đã khớp. | — |
station.title_changed | Dòng tiêu đề của một đài đã thay đổi. | filters.station_ids (≤ 200) |
track.started | Một bản nhạc đã liên kết bắt đầu phát. | filters.track_ids hoặc artist_ids (≤ 200) |
artist.started | Một nghệ sĩ đã liên kết bắt đầu phát. | filters.track_ids hoặc artist_ids (≤ 200) |
Bộ lọc cũng có thể liệt kê countries và classification. Bộ lọc chỉ theo quốc gia và luồng đổi tiêu đề không lọc dành cho Business. Mỗi endpoint có ngân sách hằng ngày là 50.000 sự kiện với Pro và 500.000 với Business; vượt quá mức đó, endpoint chuyển sang paused_budget cho đến ngày UTC tiếp theo và bạn nhận được 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
Phản hồi mang secret dùng để ký (whsec_…) một lần duy nhất. Các lần gửi tuân theo Standard Webhooks: header webhook-id, webhook-timestamp và webhook-signature: v1,<base64>, trong đó chữ ký là HMAC-SHA256 dùng các byte của secret trên {webhook-id}.{webhook-timestamp}.{body}. Hãy từ chối timestamp lệch quá 5 phút, và dùng webhook-id để loại bỏ bản trùng: giá trị này giữ nguyên qua các lần thử lại.
{ "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_…" } }
Hãy trả lời bằng bất kỳ mã 2xx nào trong vòng 10 giây. Chuyển hướng không được theo. Các lần gửi thất bại được thử lại sau 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h và 24 h, tổng cộng khoảng ba ngày, lần lượt từng lần gửi cho mỗi endpoint. Endpoint trả về 410, hoặc thất bại năm ngày liên tiếp, sẽ bị vô hiệu hóa và bạn nhận được e-mail; hãy đặt status trở lại enabled bằng PATCH khi đã sửa xong.
POST /v1/webhooks/{id}/rotate-secret trả về một secret mới; trong 24 giờ, mỗi lần gửi mang hai chữ ký, mỗi chữ ký ứng với một secret. POST /v1/webhooks/{id}/test gửi một ping. GET /v1/webhooks/{id}/deliveries?status=pending|succeeded|failed liệt kê các lần gửi trong 30 ngày, và POST …/deliveries/{dl_id}/redeliver gửi lại một lần. Khóa thử nghiệm (tp_test_…) chỉ được trỏ tới webhook.site.
GET/v1/stream khóa · developer + text/event-stream
Luồng sự kiện
Cùng các sự kiện đó qua một phản hồi HTTP kéo dài, theo định dạng server-sent events, dành cho máy chủ và ứng dụng native. Truyền station_ids (tối đa 100) hoặc wishlist_id (all, hoặc một 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
| Tham số | Mặc định | Ý nghĩa |
|---|---|---|
station_ids | — | Các id đài cách nhau bằng dấu phẩy, tối đa 100. |
wishlist_id | — | all hoặc một id danh sách mong muốn. Gửi wishlist.matched. |
events | station.title_changed | Với luồng theo đài, bất kỳ sự kiện nào trong station.title_changed, track.started, artist.started. |
classification | song | song hoặc any, với luồng theo đài. |
data là cùng JSON mà một webhook nhận được. Một comment : ping được gửi sau 20 giây không có sự kiện, vì vậy hãy đặt idle timeout của client từ 60 giây trở lên. Khi kết nối bị ngắt, hãy kết nối lại với header Last-Event-ID đặt bằng id cuối cùng bạn thấy: các sự kiện trong 10 phút gần nhất sẽ được phát lại. Một vị trí cũ hơn sẽ nhận event: gap kèm id sớm nhất còn có; hãy lấp khoảng trống từ lịch sử hoặc feed kết quả khớp. Số luồng mở cho mỗi khóa: Developer 1, Pro 5, Business 20, Tapelark 1 cho mỗi thiết bị. Thêm một luồng nữa sẽ nhận 429 too_many_connections.
Trình duyệt không thể dùng EventSource ở đây, vì nó không gửi được header Authorization và khóa không bao giờ được chấp nhận trong URL. Thay vào đó, hãy đọc luồng bằng fetch().
Trường và enum
- classification enum
song·ident(tên hoặc khẩu hiệu của đài) ·programme·ad_break·empty·junk·unknown. Chỉ bài hát mới trở thành lịch sử phát; mọi thứ đều được giữ trong dòng trực tiếp.- match_grade enum
high·medium·none. Mức độ tiêu đề được liên kết tốt tới một recording của MusicBrainz.- confidence enum
high·medium·low. Tính cho từng dòng từ classification, match grade, mức độ gần đây đài được xác nhận và lịch sử độ tin cậy của đài. Được công bố để bạn có thể kiểm tra.- method enum
- Cách Tuneping đọc đài:
icy(metadata trong luồng),icecast_json(JSON trạng thái của máy chủ),shoutcast_stats(XML trạng thái),shoutcast_currentsong,seven_html. Đây là mã định danh nội bộ, không phải sự xác nhận hay quảng bá; nếu hiển thị, hãy ghi là "trong luồng" và "trang trạng thái". - recordability enum
- Luồng của đài có phải là luồng HTTP thuần, ghi được, xét từ góc độ một client ghi âm hay không. Chỉ mang tính thông tin.
- Id
- Đài dùng UUID của Radio Browser. Bản nhạc là
trk_…, nghệ sĩart_…, sự kiện phátpe_…, khóakey_…, chủ sở hữuown_…, danh sách mong muốnwl_…, kết quả khớpwm_…, webhookwh_…, lần gửidl_…, sự kiệnevt_…. Hãy coi tất cả là chuỗi mờ (opaque). - Timestamp
- RFC 3339, UTC, độ chính xác mili giây nếu nguồn có.
OpenAPI
Mô tả cho máy đọc của API này nằm tại /docs/openapi-v1.yaml (OpenAPI 3.1). Đây là nguồn để sinh client; các operation id ổn định.
Ghi nguồn và sử dụng hợp lý
Việc sử dụng ở gói Free và ẩn danh phải hiển thị một liên kết "Data by Tuneping" dễ thấy ở nơi dữ liệu được hiển thị (huy hiệu). Lượt phát được phát hiện từ metadata của đài và không phải là giám sát được chứng nhận; hãy ghi "phát hiện từ metadata của đài" khi bạn trình bày chúng làm bằng chứng. Tên đài là văn bản; Tuneping không cung cấp logo hay ảnh bìa. Xem điều khoản.