Tuneping API v1
인터넷 라디오 방송국이 지금 재생 중인 곡과 재생했던 곡을 위한 JSON API입니다. 기본 URL은 https://api.tuneping.com/v1입니다. 메타데이터만 다룹니다: 이 서비스는 각 스트림의 공개 제목 줄만 읽으며 오디오에는 절대 손대지 않습니다.
빠른 시작
지금 Madonna를 재생하고 있는 곳은?
curl "https://api.tuneping.com/v1/now-playing/search?q=madonna&limit=3"
id를 알고 있는 방송국의 지금 재생 중인 곡(id는 Radio Browser 방송국 UUID입니다):
curl "https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/now-playing"
방송국이 최근 24시간 동안 재생한 곡(기록 권한이 있는 키, 즉 Developer 플랜 이상이 필요합니다):
curl -H "Authorization: Bearer tp_live_…" \
"https://api.tuneping.com/v1/stations/159824f0-5475-4263-bcc8-59a48a213896/history"
모든 응답은 JSON입니다. 오류는 application/problem+json입니다(오류 참조). 알 수 없는 쿼리 파라미터는 무시되지 않고 400으로 거부되므로 오타가 바로 드러납니다.
키 받기
tuneping.com/signup에서 직접 가입합니다. 이메일 주소를 입력하고, 메일로 받은 링크를 60분 이내에 열면 키가 한 번 표시됩니다. 같은 두 단계를 API로도 할 수 있습니다. {"email": "…", "plan": "free|developer|pro"}로 POST /v1/signup을 호출하면 202를 응답하고 메일을 보냅니다. 링크에 있는 {"token": "…"}로 POST /v1/signup/verify를 호출하면 201과 함께 키를 응답합니다. Free는 그 외에 필요한 것이 없습니다. Developer와 Pro는 응답에 포함된 링크를 통해 Stripe Checkout으로 이어집니다. 결제 후 1분 이내에 소유자의 플랜이 바뀌며, 소유자의 모든 키에 반영됩니다.
키를 잃어버렸나요? 같은 주소로 다시 가입하면 키가 교체되고, 이전 키는 24시간 동안 계속 작동합니다. 실시간 키는 Free와 Developer에서 하나, Pro에서 다섯 개, Business에서 스무 개이며, 이와 별도로 모든 플랜에 tp_test_ 키가 하나씩 있습니다. 포털이 출시되기 전까지는 요청 시 발급합니다.
인증
키를 bearer 토큰으로 보내거나 X-Api-Key에 담아 보내세요:
Authorization: Bearer tp_live_a1b2c3…
X-Api-Key: tp_live_a1b2c3…
키는 tp_live_ 또는 tp_test_로 시작하고 그 뒤에 43자가 이어집니다. 키 없는 요청은 익명으로 취급되어 IP 주소별로 제한됩니다. 형식이 잘못되었거나, 알 수 없거나, 폐기된 키는 익명으로는 작동했을 엔드포인트에서도 즉시 401을 응답합니다. 아래에서 키 표시가 있는 엔드포인트는 키 없이 호출하면 401 unauthenticated를 응답합니다.
스코프. 읽기 전용 등 더 좁은 목록으로 발급되지 않은 한, 키는 모든 스코프(read, exports, webhooks, wishlists)를 가집니다. GET /v1/me에서 키의 스코프를 확인할 수 있습니다. 위시리스트와 웹훅에는 wishlists와 webhooks 스코프가 필요합니다(webhook 전달을 쓰는 위시리스트는 둘 다 필요하며, 플랜보다 먼저 확인됩니다). 스코프가 없는 키는 required_scope와 함께 403 forbidden_scope를 응답하고, WWW-Authenticate에는 error="insufficient_scope"가 담깁니다.
테스트 키. tp_test_ 키는 Free 제한으로 같은 실시간 데이터를 읽으며, 자체 위시리스트, 매치, 웹훅 엔드포인트(livemode: false)를 가집니다. 라이브 쪽 데이터는 보거나 변경하지 않으므로, 테스트 스위트에서 프로덕션에 영향을 주지 않고 빈 위시리스트 세트를 PUT할 수 있습니다. 테스트 모드 웹훅은 webhook.site만 가리킬 수 있습니다.
재시도. 키나 파트너 토큰이 필요한 엔드포인트의 POST, PATCH, DELETE는 Idempotency-Key 헤더(논리적 요청마다 UUID 하나)를 받습니다. 가입 엔드포인트는 이 헤더를 받지 않습니다. 같은 키와 같은 요청으로 재시도하면 두 번째 위시리스트나 엔드포인트를 만드는 대신 처음의 성공 응답을 Idempotent-Replayed: true 표시와 함께 다시 받습니다. 키는 24시간 동안 기억됩니다. 다른 요청에 재사용하면 422 idempotency_key_reused, 첫 요청이 아직 실행 중일 때 재시도하면 409 idempotency_in_progress입니다. 오류는 기억되지 않으므로 오류 후의 재시도는 다시 실행됩니다. 헤더가 있는 요청은 연결이 끊겨도 취소되지 않습니다. 요청은 끝까지 실행되고 성공 응답이 기억되므로 재시도하면 그 응답을 받습니다. 20초가 지나도 실행 중인 요청은 아무것도 변경하지 않고 취소되며 Retry-After: 1과 함께 503 unavailable을 응답합니다. 같은 키로 재시도하세요.
파트너 앱(현재 Tapelark)은 키 대신 서명된 이용 권한 토큰을 보냅니다: Authorization: Bearer TPE1-…. 토큰은 파트너의 Ed25519 키로 오프라인에서 검증되며, 아래 표의 파트너 플랜으로 매핑됩니다. API는 최종 사용자가 누구인지 알지 못합니다. 형식이 잘못되었거나 알 수 없는 토큰은 401 token_invalid, 만료된 토큰은 401 token_expired(갱신하세요), 다른 대상용 토큰은 401 token_audience, 만료되거나 폐기된 이용 권한은 403 entitlement_revoked를 응답합니다.
속도 제한과 할당량
| 플랜 | 속도 | 할당량 | 일괄 조회 id | 기록 |
|---|---|---|---|---|
| 익명(IP당) | 2 req/s, 버스트 5 | 1,000 / 일 | 20 | 없음 |
| Free | 2 req/s, 버스트 5 | 1,000 / 일 | 20 | 없음 |
| Developer | 10 req/s, 버스트 20 | 100,000 / 월 | 100 | 7일 |
| Pro | 50 req/s, 버스트 100 | 1,000,000 / 월 | 100 | 12개월 |
| Business | 맞춤 | 맞춤 | 100 | 12개월 |
| Tapelark(이용 권한 토큰, 라이선스당) | 5 req/s, 버스트 10 | 2,000 / 일 | 50 | 7일 |
초당 제한은 토큰 버킷 방식입니다. 버킷이 비면 Retry-After와 함께 429 rate_limited를 받습니다. 할당량을 모두 쓰면 429 quota_exceeded를 받으며, Retry-After는 기간이 초기화될 때까지의 초로 설정됩니다.
공유 캐시가 불가능한 인증 응답에는 IETF 속도 제한 필드가 담깁니다:
RateLimit-Policy: "burst";q=10;w=1, "monthly";q=100000;w=2592000
RateLimit: "burst";r=7;t=1
공유 캐시가 가능한 응답(검색, 일괄 조회, 방송국 지금 재생 중, 방송국, 통계, 상태 시계열)에는 속도 제한 헤더가 없습니다. 엣지가 같은 본문을 다른 호출자에게 제공할 수 있기 때문입니다. 남은 사용량은 언제든 GET /v1/me에서 확인할 수 있습니다.
캐싱
실시간 응답은 5초 동안 캐시할 수 있으며(Cache-Control: public, max-age=5, s-maxage=5) 약한 ETag가 붙습니다. If-None-Match를 보내면 변경 사항이 없을 때 304를 받습니다. 304는 오리진에서 할당량에 포함되지 않습니다. 방송국 디렉터리는 60초, 단일 방송국은 300초 동안 캐시됩니다. 기록은 private, max-age=30입니다. 상태 엔드포인트와 키 엔드포인트는 no-store입니다.
모든 응답에는 X-Request-Id가 붙습니다. 지원팀에 문의할 때 함께 알려 주세요.
오류
오류는 RFC 9457 problem details를 따르며 확장 멤버 두 가지가 있습니다. 안정적인 기계 판독용 문자열인 code, 그리고 유효성 검사 실패 시의 errors[]나 403 시의 required_tier 같은 엔드포인트별 추가 필드입니다.
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." } ]
}
전체 코드 목록과 의미는 오류 카탈로그에 있습니다. 가장 먼저 만나게 될 코드는 다음과 같습니다:
| HTTP | code | 발생 조건 |
|---|---|---|
| 400 | invalid_request | 알 수 없는 쿼리 파라미터 또는 잘못된 형식의 요청 |
| 400 | invalid_cursor | 커서의 형식이 잘못되었거나, 만료되었거나(24시간, 매치 피드 since_cursor는 30일), 다른 필터와 함께 사용됨 |
| 401 | unauthenticated · invalid_api_key · key_revoked | 키가 없거나, 알 수 없거나, 폐기됨 |
| 403 | forbidden_tier | 엔드포인트나 파라미터에 더 높은 플랜이 필요함. required_tier가 어떤 플랜인지 알려 줌 |
| 403 | forbidden_scope | 키가 해당 스코프 없이 발급됨. required_scope가 어떤 스코프인지 알려 줌 |
| 404 | not_found · station_opted_out | 알 수 없는 id, 또는 방송국이 인덱싱하지 말 것을 요청함 |
| 422 | validation_failed | errors[]에 필드 오류가 있음 |
| 429 | rate_limited · quota_exceeded | 속도를 줄이세요. Retry-After는 초 단위 |
| 503 | unavailable | 데이터베이스를 사용할 수 없거나, Idempotency-Key가 있는 요청이 20초 후 취소됨. Retry-After 후에 재시도 |
GET/v1/now-playing/search 키 불필요 public, max-age=5 · ETag
실시간 검색
지금 q를 재생하고 있는 방송국을 찾습니다. 퍼지 검색이며 악센트를 구분하지 않습니다. gronemeyer manner로 Grönemeyer – Männer를 찾을 수 있습니다. 행은 유사도순, 그다음 제목이 나타난 시점이 최근인 순으로 정렬됩니다. 페이지네이션은 없습니다. 인덱스가 몇 초마다 바뀌어 페이지를 나누면 항목이 건너뛰어지거나 반복되기 때문입니다. 대신 limit을 늘리세요.
| 파라미터 | 타입 | 기본값 | 의미 |
|---|---|---|---|
q | 문자열, 2–120자 | 필수 | 아티스트, 제목 또는 둘 다. 세 글자 이상이면 트라이그램 매칭을, 그보다 짧으면 부분 문자열 매칭을 사용합니다. |
country | ISO 3166-1 alpha-2, 반복 가능 | 전체 | 이 국가들의 방송국만. |
codec | 문자열 | 전체 | 예: MP3, AAC. |
min_bitrate | 정수 | 0 | kbit/s. |
classification | enum | song | song, ident, programme, ad_break, empty, junk, unknown 또는 any. |
max_age_seconds | 30–900 | 180 | 방송국이 이 시간(초) 이내에 성공적으로 폴링된 행만. |
limit | 1–100 | 20 | 반환할 행 수. truncated: true는 결과가 더 있었음을 뜻합니다. |
응답 · 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 }
}
행 읽는 법
- started_at RFC 3339 · 추정치
- 현재 제목의 추정 시작 시각이며, 앞뒤로 ±
start_uncertainty_seconds의 오차가 있습니다. - observed_at RFC 3339
- Tuneping이 이 제목을 처음 확인한 시각.
- confirmed_at RFC 3339
- 방송국의 가장 최근 성공한 폴링, 즉 “이 시점까지 재생 중”. 최신성 판단에 사용하세요.
- track 객체 · 선택
- 제목이 MusicBrainz 레코딩에 연결된 경우에 있습니다.
- similarity 0–1
- 원하는 임곗값으로 잘라 쓰세요.
GET/v1/now-playing 키 불필요 public, max-age=5 · ETag
일괄 지금 재생 중
즐겨찾기 화면을 위해 방송국 목록의 현재 제목을 한 번의 호출로 가져옵니다.
| 파라미터 | 타입 | 기본값 | 의미 |
|---|---|---|---|
station_ids | 쉼표로 구분한 UUID | 필수 | 익명 또는 Free는 최대 20개, Developer부터는 100개. id는 캐싱 전에 정렬되고 중복이 제거되므로, 즐겨찾기가 같은 두 클라이언트는 캐시된 본문 하나를 공유합니다. |
max_age_seconds | 30–86400 | 900 | 이보다 오래된 행도 반환되지만 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 키 불필요
방송국 지금 재생 중
방송국 하나에 대한 행 하나입니다. 아직 확인된 제목이 없으면 404 not_found, 방송국이 인덱싱하지 말 것을 요청했으면 404 station_opted_out입니다.
GET/v1/stations GET/v1/stations/{id} 키 불필요 max-age=60 · 300
방송국
커뮤니티가 관리하는 Radio Browser 디렉터리를 바탕으로 만들고, Tuneping이 각 방송국을 읽는 방식 정보를 더한 디렉터리입니다.
| 파라미터 | 타입 | 기본값 | 의미 |
|---|---|---|---|
q | 문자열 | — | 방송국 이름, 퍼지 검색. |
country | alpha-2, 반복 가능 | 전체 | |
tag | 문자열, 반복 가능 | 전체 | 지정한 태그가 모두 일치해야 합니다. |
codec, min_bitrate | 검색과 동일. | ||
has_now_playing | 불리언 | false | Tuneping이 현재 제목을 읽고 있는 방송국만. |
sort | enum | clickcount | clickcount(인기순, 내림차순), name, bitrate. |
limit | 1–200 | 50 | |
cursor | 불투명 문자열 | — | 이전 페이지의 next_cursor에서 가져옵니다. 24시간 동안 유효하며 같은 필터로만 사용할 수 있습니다. |
{ "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은 디렉터리의 값을 텍스트로 그대로 전달할 뿐이며, Tuneping은 로고를 복사하거나 제공하지 않습니다.
GET/v1/stations/{id}/history 키 · Developer 이상 private, max-age=30
재생 기록
방송국이 재생한 곡을 최신순으로, 추정 시작·종료 시각과 함께 제공합니다. 조회 가능한 기간은 플랜에 따라 다릅니다. Developer는 7일, Pro와 Business는 12개월입니다. 그보다 오래된 from을 요청하면 기간을 몰래 잘라 내는 대신 required_tier와 함께 403 forbidden_tier를 응답합니다.
| 파라미터 | 타입 | 기본값 | 의미 |
|---|---|---|---|
from, to | RFC 3339 | to = 현재, from = to − 24 h | 호출당 최대 31일. |
classification | enum | song | 검색과 동일. |
limit | 1–200 | 50 | |
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 키 no-store
키와 사용량
/v1/me는 키, 소유자의 플랜과 상태, 적용되는 제한, 현재 기간의 지금까지 사용량을 반환합니다. /v1/me/usage?from=&to=는 일별 요청 수를 반환합니다(오리진 요청만 해당하며, 엣지 캐시 히트는 집계되지 않습니다).
{ "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 키 불필요
상태와 통계
/v1/health는 status가 ok 또는 degraded인 200을 응답하고, 데이터베이스가 다운되면 503을 응답합니다. 가동 시간 모니터가 이 엔드포인트를 가리키게 하세요. /v1/health/series?minutes=60(5–1440)은 분 단위 폴러 지표를 반환합니다: polls, ok, titles_changed, songs, bytes_in, bytes_out, latency_p50_ms, latency_p90_ms, inflight_max, ips_paused, queue_lag_seconds 및 by_method 세부 내역. /v1/stats는 상태 페이지에 표시되는 공개 커버리지 스냅숏입니다: 전체 방송국 수, 폴링 중인 방송국 수, 실시간 메타데이터가 있는 방송국 수, 분당 제목 변경 수, 곡 비율, 방식별, 국가별.
POST/v1/wishlists PUT/v1/wishlists GET/v1/wishlists/matches 키 · Developer 이상 no-store
위시리스트
곡, 아티스트 또는 자유 텍스트 제목을 지정하면 방송국이 그것을 재생하기 시작할 때 알 수 있습니다. 모든 제목 변경은 읽히는 즉시 위시리스트와 대조되며, 새 위시리스트는 30초 이내에 적용됩니다. 위시리스트는 Developer 10개, Pro 100개, Business 1,000개까지 둘 수 있습니다. 매치는 30일 동안 보관됩니다.
| 필드 | 타입 | 의미 |
|---|---|---|
kind | artist · track · query | artist와 track은 검색에서 얻은 id(art_…, trk_…)를 받으며, 정규화기가 그 id에 연결하는 다른 표기도 잡아냅니다. query는 자유 텍스트를 받으며, 보통 Artist - Title 형식입니다. |
value | 문자열, ≤ 240 | id 또는 텍스트. |
match | exact · fuzzy | query 전용. exact는 정규화된 텍스트를 비교하고, fuzzy는 트라이그램 유사도 0.6 이상을 허용합니다. |
station_filter | 객체 | station_ids(≤ 500) 및/또는 countries(alpha-2, 대소문자 무관, 대문자로 반환). 제목의 방송국이 목록에 있거나 목록의 국가에 속하면 매치됩니다. 필터가 없으면 모든 방송국이 대상입니다. |
delivery | 배열 | poll(기본값), sse, webhook. 웹훅 전달은 Pro 이상이며, webhook_id와 webhooks 스코프가 있는 키가 필요합니다. |
description | 문자열, ≤ 200 | 자유롭게 사용하는 메모. |
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
station_ids 목록을 명시하면, 위시리스트가 활성화된 동안 Tuneping은 해당 방송국을 60초가 아닌 30초마다 읽으며, 응답에 tightens: true로 이를 알려 줍니다. 목록에 지정한 방송국의 합집합은 플랜별로 상한이 있습니다(/v1/me의 limits.tightened_stations). 이를 초과하는 위시리스트는 code가 tightened_stations_exceeded인 errors[] 항목(필드는 station_filter.station_ids, PUT의 경우 data)과 함께 422 validation_failed를 응답합니다. 국가 필터는 여전히 매치되며, 각 방송국의 일반 간격으로 적용됩니다.
GET /v1/wishlists/{id}, PATCH(방송국 필터, match, delivery, webhook, description, status: enabled | paused), DELETE는 예상대로 작동합니다. {"data": [ … ]}로 PUT /v1/wishlists를 호출하면 전체 세트를 한 번에 교체합니다. 변경되지 않은 항목은 id와 매치 수가 유지되고, 나머지는 생성되거나 삭제됩니다. 앱이 로컬에 보관하는 목록을 동기화하는 가장 쉬운 방법입니다. 이 요청의 본문은 최대 1 MiB까지 가능하며, 다른 요청 본문은 64 KiB로 제한됩니다(413 payload_too_large).
매치는 tail 피드입니다. GET /v1/wishlists/matches는 모든 위시리스트를, GET /v1/wishlists/{id}/matches는 그중 하나를 다룹니다. 행은 오래된 순으로 옵니다. 각 응답의 next_cursor를 보관했다가 since_cursor로 다시 보내세요. 이 값은 null이 되지 않으며, 30일 보관 기간 동안 유효하고 재시작 후에도 유지됩니다. 커서 없이 요청하면 최신 매치를 받습니다. limit은 1–500이며 기본값은 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은 제목이 확인된 시점을 기준으로 이미 보정되어 있으므로, 녹음 앱은 “약 N ± U초 전에 시작됨”이라고 표시할 수 있습니다. expected_end_hint_at은 started_at에 곡 길이 중앙값인 222초를 더한 값입니다. 예측이 아니라 참고용 힌트입니다. confidence는 연결된 트랙 또는 아티스트 id를 통해 매치되었으면 high, 정규화된 아티스트와 제목으로 매치되었으면 medium, 텍스트 쿼리면 low입니다.
POST/v1/webhooks GET/v1/webhooks/{id}/deliveries 키 · Pro 이상 no-store
웹훅
Tuneping은 이벤트가 발생하는 즉시 HTTPS 엔드포인트로 POST합니다. 엔드포인트는 Pro 5개, Business 20개입니다.
| 이벤트 | 발생 조건 | Pro 필요 조건 |
|---|---|---|
wishlist.matched | webhook_id에 이 엔드포인트를 지정한 위시리스트가 매치됨. | — |
station.title_changed | 방송국의 제목 줄이 바뀜. | filters.station_ids(≤ 200) |
track.started | 연결된 트랙이 시작됨. | filters.track_ids 또는 artist_ids(≤ 200) |
artist.started | 연결된 아티스트의 곡이 시작됨. | filters.track_ids 또는 artist_ids(≤ 200) |
필터에는 countries와 classification도 지정할 수 있습니다. 국가만 지정한 필터와 필터 없는 제목 변경 스트림은 Business 전용입니다. 각 엔드포인트의 일일 이벤트 한도는 Pro 50,000건, Business 500,000건입니다. 이를 넘으면 다음 UTC 날짜까지 엔드포인트가 paused_budget 상태가 되며 이메일이 발송됩니다.
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
응답에는 서명용 secret(whsec_…)이 한 번만 담깁니다. 전달은 Standard Webhooks를 따릅니다. 헤더는 webhook-id, webhook-timestamp, webhook-signature: v1,<base64>이며, 서명은 시크릿의 바이트로 {webhook-id}.{webhook-timestamp}.{body}에 대해 계산한 HMAC-SHA256입니다. 5분 넘게 차이 나는 타임스탬프는 거부하고, webhook-id로 중복을 걸러 내세요. 이 값은 재시도 간에 동일합니다.
{ "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_…" } }
10초 이내에 2xx 중 아무 응답이나 보내세요. 리디렉션은 따라가지 않습니다. 실패한 전달은 5초, 5분, 30분, 2시간, 5시간, 10시간, 14시간, 20시간, 24시간 후에 재시도되며, 모두 합쳐 약 3일이고 엔드포인트당 한 번에 하나씩 보냅니다. 410을 응답하거나 5일 연속 실패한 엔드포인트는 비활성화되며 이메일이 발송됩니다. 문제를 해결한 뒤 PATCH로 status를 다시 enabled로 설정하세요.
POST /v1/webhooks/{id}/rotate-secret은 새 시크릿을 반환합니다. 24시간 동안 모든 전달에 시크릿별로 하나씩, 두 개의 서명이 붙습니다. POST /v1/webhooks/{id}/test는 ping을 보냅니다. GET /v1/webhooks/{id}/deliveries?status=pending|succeeded|failed는 30일간의 전달 내역을 나열하고, POST …/deliveries/{dl_id}/redeliver는 하나를 다시 보냅니다. 테스트 키(tp_test_…)는 webhook.site만 가리킬 수 있습니다.
GET/v1/stream 키 · Developer 이상 text/event-stream
이벤트 스트림
같은 이벤트를 server-sent events 형식의 긴 HTTP 응답 하나로 받습니다. 서버와 네이티브 앱용입니다. station_ids(최대 100개) 또는 wishlist_id(all 또는 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
| 파라미터 | 기본값 | 의미 |
|---|---|---|
station_ids | — | 쉼표로 구분한 방송국 id, 최대 100개. |
wishlist_id | — | all 또는 위시리스트 id. wishlist.matched를 보냅니다. |
events | station.title_changed | 방송국 스트림의 경우 station.title_changed, track.started, artist.started 중 아무것이나. |
classification | song | 방송국 스트림의 경우 song 또는 any. |
data는 웹훅이 받는 것과 같은 JSON입니다. 이벤트 없이 20초가 지나면 : ping 주석이 도착하므로, 클라이언트의 유휴 타임아웃을 60초 이상으로 설정하세요. 연결이 끊기면 마지막으로 받은 id를 Last-Event-ID 헤더에 넣어 다시 연결하세요. 최근 10분 동안의 이벤트가 재전송됩니다. 그보다 오래된 위치에는 사용 가능한 가장 이른 id와 함께 event: gap이 전달되므로, 빈 구간은 기록이나 매치 피드로 채우세요. 키당 열 수 있는 스트림 수는 Developer 1개, Pro 5개, Business 20개, Tapelark 기기당 1개입니다. 하나를 더 열면 429 too_many_connections를 응답합니다.
브라우저에서는 여기서 EventSource를 사용할 수 없습니다. Authorization 헤더를 보낼 수 없고, 키는 URL로 절대 받지 않기 때문입니다. 대신 fetch()로 스트림을 읽으세요.
필드와 열거형
- classification enum
song·ident(방송국 이름 또는 슬로건) ·programme·ad_break·empty·junk·unknown. 곡만 재생 기록이 되며, 실시간 행에는 모든 것이 유지됩니다.- match_grade enum
high·medium·none. 제목이 MusicBrainz 레코딩에 얼마나 잘 연결되었는지.- confidence enum
high·medium·low. 분류, 매치 등급, 방송국이 얼마나 최근에 확인되었는지, 방송국의 과거 실적을 바탕으로 행마다 계산됩니다. 검증할 수 있도록 문서로 공개합니다.- method enum
- Tuneping이 방송국을 읽는 방식:
icy(스트림 내 메타데이터),icecast_json(서버 상태 JSON),shoutcast_stats(상태 XML),shoutcast_currentsong,seven_html. 내부 식별자이며 특정 제품을 보증하는 것이 아닙니다. 화면에 표시한다면 “스트림 내”, “상태 페이지”로 표시하세요. - recordability enum
- 녹음 클라이언트 관점에서 방송국의 스트림이 녹음 가능한 일반 HTTP 스트림인지 여부. 참고용입니다.
- ID
- 방송국은 Radio Browser UUID를 사용합니다. 트랙은
trk_…, 아티스트는art_…, 재생 이벤트는pe_…, 키는key_…, 소유자는own_…, 위시리스트는wl_…, 매치는wm_…, 웹훅은wh_…, 전달은dl_…, 이벤트는evt_…입니다. 모두 불투명 문자열로 취급하세요. - 타임스탬프
- RFC 3339, UTC, 원본에 있는 경우 밀리초 정밀도.
OpenAPI
이 API의 기계 판독용 명세는 /docs/openapi-v1.yaml(OpenAPI 3.1)에 있습니다. 클라이언트 생성의 기준이 되며, operation id는 변경되지 않습니다.
출처 표시와 공정 이용
Free 플랜과 익명 사용 시에는 데이터가 표시되는 곳에 눈에 보이는 “Data by Tuneping” 링크를 표시해야 합니다(배지). 재생은 방송국 메타데이터에서 감지한 것이며 인증된 모니터링이 아닙니다. 근거로 제시할 때는 “방송국 메타데이터에서 감지됨”이라고 밝히세요. 방송국 이름은 텍스트이며, Tuneping은 로고나 아트워크를 제공하지 않습니다. 이용약관을 참고하세요.