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

Ví dụ header phản hồi đầy đủ
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).
HeaderKiểuÝ nghĩa
X-RateLimit-Limitsố nguyênTổng số request cho phép trong một cửa sổ 60 giây với key này.
X-RateLimit-Remainingsố nguyênSố 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-Usedsố nguyênSố request đã dùng trong cửa sổ hiện tại. Luôn bằng Limit trừ Remaining.
X-RateLimit-Resetunix 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-Aftersố giâySố 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-Windowsố giâyĐộ dài cửa sổ, hiện là 60.
X-RateLimit-PolicychuỗiMô 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.
RateLimitchuỗiBản chuẩn IETF draft: limit=120, remaining=117, reset=42. Nhiều thư viện HTTP đọc tự động header này.
RateLimit-PolicychuỗiBản chuẩn IETF của X-RateLimit-Policy.
Retry-Aftersố giâyChỉ 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.
Tất cả các header trên đều nằm trong 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.

Nhóm endpoint unmetered
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 429 Too Many Requests
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
}
Đừng thử lại ngay lập tức. Vòng lặp retry không chờ sẽ giữ bạn ở trạng thái 429 vô hạn và có thể khiến key bị tạm khoá. Luôn ngủ đúng số giây trong 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.

Node.js — XuniBoxClient
/**
 * 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 — cURL + Retry-After
<?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

Python — requests.Session
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.

Node.js — cache 5 phút ở backend
// Ướ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ệuTTL cache khuyến nghịLý do
/tracks, /artists, /genres5–15 phútKho nhạc thay đổi theo giờ chứ không theo giây.
/search, /search/suggest60 giây theo từ khoáNgười dùng gõ lặp lại rất nhiều từ khoá giống nhau.
/trending15 phútBảng xếp hạng tính theo ngày, không cần realtime.
/tracks/:id/waveform, /tracks/waveforms24 giờSóng âm gần như không đổi sau khi bài được đăng.
/licenses, /tracks/:id/license6–24 giờĐiều kiện bản quyền rất ít khi thay đổi.
/tracks/:id/similar1 giờGợi ý tính từ metadata, thay đổi chậm.
/tracks/:id/audio-urlKHÔNG cacheLink ký hạn chỉ sống 1 giờ, cache là hỏng.
/tracks/:id/watermarkedKHÔNG cacheMỗi lượt tải là một mã watermark riêng để truy vết.
/webhooksKHÔNG cacheDữ liệu cấu hình của riêng đối tác.
/me/usageKHÔNG cacheLuô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.

Request
curl --request GET \
  --url 'https://xunibox.vn/api/public/me/usage?days=30' \
  --header 'X-API-Key: xnb_live_your_key'
Response
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 }
  ]
}
Cần nâng hạn mức cho đợt đồng bộ ban đầu hoặc cho chiến dịch lớn? Gửi yêu cầu qua form xin quyền tích hợp, ghi rõ số request/phút dự kiến và lý do. Bạn cũng có thể xem lịch sử usage ngay trong cổng đối tác.

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.

Xin quyền tích hợp