Tài liệu API XuniBOX
API chỉ đọc để lấy danh sách nhạc, nghệ sĩ và thể loại từ kho XuniBOX. Mỗi chủ đề dưới đây là một trang riêng, có đường dẫn riêng để bạn chia sẻ cho đội kỹ thuật.
Hạn mức & header RateLimit
Cập nhật mới nhất: mọi endpoint dữ liệu đều cần API key và đều tính vào hạn mức của key. Ngoài ra còn hạn mức theo IP cho mọi request. Hạn mức chặt hơn áp dụng cho /tracks/:id/watermarked (kèm hạn mức riêng theo phút và theo ngày cho mỗi key), /me/usage và /webhooks. Mặc định 60 request/phút mỗi key, admin có thể nâng riêng. Client của bạn vẫn nên xử lý được 429 vì XuniBOX có thể bật hạn mức cho nhóm công khai khi lưu lượng tăng.
Header trên phản hồi của endpoint có hạn mức
HTTP/1.1 200 OK
Content-Type: audio/mpeg
Cache-Control: no-store
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Used: 3
X-RateLimit-Reset: 1785921060
X-RateLimit-Reset-After: 42
X-RateLimit-Window: 60
X-RateLimit-Policy: 120;w=60
RateLimit: limit=120, remaining=117, reset=42
RateLimit-Policy: 120;w=60
# Bộ header này xuất hiện ở các endpoint cần API key
# (hiện tại là GET /tracks/:id/watermarked).| Header | Kiểu | Ý nghĩa |
|---|---|---|
| X-RateLimit-Limit | số nguyên | Tổng số request cho phép trong một cửa sổ 60 giây với key này. |
| X-RateLimit-Remaining | số nguyên | Số request còn lại trong cửa sổ hiện tại. Bằng 0 nghĩa là request tiếp theo sẽ nhận 429. |
| X-RateLimit-Used | số nguyên | Số request đã dùng trong cửa sổ hiện tại. Luôn bằng Limit trừ Remaining. |
| X-RateLimit-Reset | unix epoch (giây) | Mốc thời gian tuyệt đối khi cửa sổ mở lại. Dùng khi bạn cần lưu trạng thái qua nhiều tiến trình. |
| X-RateLimit-Reset-After | số giây | Số giây tương đối còn lại tới lúc reset. Dễ dùng hơn Reset vì không phụ thuộc đồng hồ máy bạn. |
| X-RateLimit-Window | số giây | Độ dài cửa sổ, hiện là 60. |
| X-RateLimit-Policy | chuỗi | Mô tả chính sách theo dạng limit;w=window, ví dụ 120;w=60. Giá trị unmetered nghĩa là endpoint không tính hạn mức. |
| RateLimit | chuỗi | Bản chuẩn IETF draft: limit=120, remaining=117, reset=42. Nhiều thư viện HTTP đọc tự động header này. |
| RateLimit-Policy | chuỗi | Bản chuẩn IETF của X-RateLimit-Policy. |
| Retry-After | số giây | Chỉ xuất hiện ở phản hồi 429. Số giây tối thiểu phải chờ trước khi thử lại. |
Access-Control-Expose-Headers, nên JavaScript trên trình duyệt cũng đọc được bằng response.headers.get("X-RateLimit-Remaining") — điều mà mặc định CORS không cho phép.Endpoint không tính hạn mức
Chỉ nhóm endpoint hạ tầng phục vụ giám sát (health, healthz, readyz, status, metrics, openapi) là không trừ hạn mức của key và không cần API key; nhóm này vẫn bị giới hạn theo IP.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
ETag: "a1b2c3d4e5f6"
X-Total-Count: 248
X-Page: 1
X-Limit: 20
X-Offset: 0
# Mọi endpoint dữ liệu đều BẮT BUỘC header X-API-Key và tính vào hạn mức:
# /tracks /tracks/:id /tracks/by-ids /tracks/:id/audio-url
# /tracks/:id/license /tracks/:id/similar /tracks/:id/waveform
# /tracks/waveforms /search /search/suggest /trending /licenses
# /artists /artists/:id/tracks /genres
#
# Chỉ nhóm hạ tầng dưới đây là KHÔNG cần key (vẫn giới hạn theo IP):
# /health /healthz /readyz /status /metrics
# /openapi.json /openapi.v1.json /tracks/:id/cover
# Gọi bao nhiêu lần cũng được — nhưng hãy cache phía bạn và đừng poll
# dưới 10 giây một lần. XuniBOX có thể bật hạn mức cho nhóm này trong
# tương lai, nên client của bạn vẫn nên xử lý được 429.Khi chạm trần: phản hồi 429
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Used: 120
X-RateLimit-Reset: 1785921060
X-RateLimit-Reset-After: 37
X-RateLimit-Window: 60
{
"error": "Vượt hạn mức. Thử lại sau 37 giây.",
"retry_after": 37
}Retry-After, cộng thêm jitter ngẫu nhiên 0–500ms nếu bạn chạy nhiều tiến trình song song.Client tự điều tiết (Node.js)
Cách bền nhất là đọc header sau mỗi lần gọi và tự dừng trước khi chạm trần, thay vì chờ tới lúc bị 429.
/**
* Bộ gọi API tự điều tiết: đọc header còn lại và tự chờ khi sắp chạm trần.
* Dùng chung một instance cho toàn bộ backend của bạn.
*/
class XuniBoxClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.remaining = Infinity;
this.resetAfterMs = 0;
this.resetAt = 0;
}
async request(path, init = {}) {
// Nếu lần trước báo đã hết hạn mức thì ngủ tới lúc reset.
if (this.remaining <= 0 && Date.now() < this.resetAt) {
await new Promise((r) => setTimeout(r, this.resetAt - Date.now()));
}
const res = await fetch(`https://xunibox.vn/api/public${path}`, {
...init,
headers: { ...init.headers, "X-API-Key": this.apiKey, Accept: "application/json" },
});
// Cập nhật trạng thái hạn mức từ header mỗi phản hồi.
const remaining = res.headers.get("X-RateLimit-Remaining");
const resetAfter = res.headers.get("X-RateLimit-Reset-After");
if (remaining !== null) this.remaining = Number(remaining);
if (resetAfter !== null) this.resetAt = Date.now() + Number(resetAfter) * 1000;
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 5) * 1000;
await new Promise((r) => setTimeout(r, wait));
return this.request(path, init); // thử lại đúng một vòng sau khi chờ
}
if (!res.ok) throw new Error(`XuniBOX ${res.status}: ${await res.text()}`);
return res.json();
}
}
const xunibox = new XuniBoxClient(process.env.XUNIBOX_API_KEY);
const { data } = await xunibox.request("/tracks?limit=20");PHP / Laravel
<?php
// Laravel / PHP thuần: đọc header hạn mức và tôn trọng Retry-After.
function xuniboxGet(string $path, array $query = []): array
{
$url = 'https://xunibox.vn/api/public' . $path . '?' . http_build_query($query);
for ($attempt = 1; $attempt <= 3; $attempt++) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . env('XUNIBOX_API_KEY'),
'Accept: application/json',
],
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerLen = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($raw, 0, $headerLen);
$body = substr($raw, $headerLen);
curl_close($ch);
if ($status === 429) {
preg_match('/retry-after:\s*(\d+)/i', $headers, $m);
sleep((int) ($m[1] ?? 5));
continue;
}
if ($status >= 500) { sleep(2 ** $attempt); continue; }
return json_decode($body, true);
}
throw new RuntimeException('XuniBOX API không phản hồi sau 3 lần thử.');
}Python
import os, time, requests
BASE = "https://xunibox.vn/api/public"
SESSION = requests.Session()
SESSION.headers.update({"X-API-Key": os.environ["XUNIBOX_API_KEY"], "Accept": "application/json"})
def xunibox_get(path: str, **params):
for attempt in range(3):
res = SESSION.get(f"{BASE}{path}", params=params, timeout=10)
remaining = int(res.headers.get("X-RateLimit-Remaining", 999))
if remaining <= 2: # sắp chạm trần, tự giãn nhịp
time.sleep(int(res.headers.get("X-RateLimit-Reset-After", 1)))
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", 5)))
continue
if res.status_code >= 500:
time.sleep(2 ** attempt)
continue
res.raise_for_status()
return res.json()
raise RuntimeError("XuniBOX API không phản hồi sau 3 lần thử.")Cache ở phía bạn
Dù nhóm endpoint công khai chưa tính hạn mức, gọi thẳng XuniBOX cho từng người dùng cuối vẫn là thiết kế tồi: chậm hơn, tốn băng thông và dễ vỡ khi có sự cố mạng. Cache ở backend của bạn hầu như luôn là câu trả lời đúng. Với các endpoint cần key thì hạn mức tính theo key, không theo người dùng cuối của bạn.
// Ước lượng hạn mức cần thiết cho ứng dụng của bạn.
//
// requests/phút = (số người mở bộ chọn nhạc mỗi phút)
// x (số request mỗi phiên: 1 danh sách + 1 genres + n lần tìm kiếm)
//
// Ví dụ: 300 người/phút mở bộ chọn nhạc, mỗi người gọi trung bình 4 request
// => 1.200 req/phút, vượt xa mức mặc định 60.
//
// Cách xử lý đúng: KHÔNG xin hạn mức 1.200. Hãy cache ở backend của bạn.
// Backend của bạn (Node) — cache danh sách 5 phút, phục vụ mọi người dùng.
const cache = new Map(); // key -> { at, body }
const TTL_MS = 5 * 60 * 1000;
export async function getTracks(query) {
const key = new URLSearchParams(query).toString();
const hit = cache.get(key);
if (hit && Date.now() - hit.at < TTL_MS) return hit.body;
const res = await fetch(`https://xunibox.vn/api/public/tracks?${key}`, {
headers: { "X-API-Key": process.env.XUNIBOX_API_KEY },
});
const body = await res.json();
cache.set(key, { at: Date.now(), body });
return body;
}
// Với cache 5 phút, 300 người/phút chỉ tốn khoảng 1-2 request/phút tới XuniBOX.| Loại dữ liệu | TTL cache khuyến nghị | Lý do |
|---|---|---|
| /tracks, /artists, /genres | 5–15 phút | Kho nhạc thay đổi theo giờ chứ không theo giây. |
| /search, /search/suggest | 60 giây theo từ khoá | Người dùng gõ lặp lại rất nhiều từ khoá giống nhau. |
| /trending | 15 phút | Bảng xếp hạng tính theo ngày, không cần realtime. |
| /tracks/:id/waveform, /tracks/waveforms | 24 giờ | Sóng âm gần như không đổi sau khi bài được đăng. |
| /licenses, /tracks/:id/license | 6–24 giờ | Điều kiện bản quyền rất ít khi thay đổi. |
| /tracks/:id/similar | 1 giờ | Gợi ý tính từ metadata, thay đổi chậm. |
| /tracks/:id/audio-url | KHÔNG cache | Link ký hạn chỉ sống 1 giờ, cache là hỏng. |
| /tracks/:id/watermarked | KHÔNG cache | Mỗi lượt tải là một mã watermark riêng để truy vết. |
| /webhooks | KHÔNG cache | Dữ liệu cấu hình của riêng đối tác. |
| /me/usage | KHÔNG cache | Luôn trả Cache-Control: no-store. |
Theo dõi mức dùng: GET /me/usage
Endpoint này trả về trạng thái key, hạn mức phút hiện tại và lịch sử theo ngày. Tham số days từ 1 đến 90, mặc định 30. Dùng nó để dựng biểu đồ nội bộ hoặc cảnh báo khi mức dùng tăng bất thường.
curl --request GET \
--url 'https://xunibox.vn/api/public/me/usage?days=30' \
--header 'X-API-Key: xnb_live_your_key'HTTP/1.1 200 OK
Cache-Control: no-store
{
"key": {
"name": "Xuni App Production",
"prefix": "xnb_live_9f2c",
"is_active": true,
"created_at": "2026-01-12T03:20:11.000Z",
"last_used_at": "2026-08-06T05:58:40.113Z"
},
"organization": "Xuni Media JSC",
"account_status": "approved",
"quota": {
"requests_per_minute": 120,
"used_last_minute": 3,
"remaining_this_minute": 117
},
"totals": { "all_time": 184203, "window_days": 30, "window_requests": 51280 },
"daily": [
{ "date": "2026-08-04", "requests": 1802 },
{ "date": "2026-08-05", "requests": 1955 },
{ "date": "2026-08-06", "requests": 640 }
]
}Sẵn sàng tích hợp?
Gửi thông tin sản phẩm để admin XuniBOX xét duyệt và cấp API key.