# Phân trang > Duyệt bản phát hành theo trang với cursor và next_cursor, duyệt các lần gửi webhook với before và has_more, và chỉ đồng bộ phần đã thay đổi. Hầu hết danh sách trong API đều ngắn và được trả về trọn vẹn. Có hai danh sách có thể dài và được phân trang: bản phát hành và các lần gửi webhook. ## Danh sách nào được phân trang | Endpoint | Cách phân trang | Kích thước trang | | --- | --- | --- | | `GET /releases` | `cursor` → `next_cursor` | `limit` 1–100, mặc định 50 | | `GET /webhooks/{id}/deliveries` | `before` → `has_more` | `limit` 1–100, mặc định 50 | | `GET /ddex` | Không phân trang. 100 lần phân phối gần nhất của bạn. | 100 | | `GET /members` | Không phân trang. Mọi nghệ sĩ và hãng đĩa. | Tất cả | | `GET /links` | Không phân trang. Mọi smart link và pre-save. | Tất cả | | `GET /payouts` | Không phân trang. Mọi yêu cầu rút tiền. | Tất cả | | `GET /webhooks` | Không phân trang. Tối đa 20 endpoint. | Tất cả | | `GET /earnings` | Không phân trang. Nhóm theo `by` trong khoảng `months` (1–60). | Tất cả | ## Bản phát hành: cursor `GET /releases` trả về bản phát hành mới nhất trước. Mỗi phản hồi có `next_cursor`. Hãy gửi lại giá trị này trong `cursor` để lấy trang tiếp theo. Ở trang cuối, `next_cursor` là `null`. ```bash curl "https://api.hillsmusic.com/v1/releases?limit=100" \ -H "Authorization: Bearer $HILLS_API_KEY" # { "data": [ … ], "next_cursor": "aWQ6ODEy" } curl "https://api.hillsmusic.com/v1/releases?limit=100&cursor=aWQ6ODEy" \ -H "Authorization: Bearer $HILLS_API_KEY" ``` Hãy coi cursor là một chuỗi không cần hiểu nội dung (opaque). Đừng tự tạo hay sửa nó. Giữ nguyên các parameter khác trên mọi trang. Nếu bạn lọc trang đầu bằng `status=approved`, hãy gửi cả `status=approved` cùng với cursor. Nếu đổi bộ lọc, bạn nên bắt đầu lại từ đầu mà không có cursor. ### Lấy mọi bản phát hành bằng Node ```js async function* allReleases(params = {}) { let cursor = null; do { const url = new URL("https://api.hillsmusic.com/v1/releases"); for (const [k, v] of Object.entries({ ...params, limit: 100 })) url.searchParams.set(k, v); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.HILLS_API_KEY}` }, }); if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`); const page = await res.json(); yield* page.data; cursor = page.next_cursor; } while (cursor); } for await (const r of allReleases({ status: "approved" })) { console.log(r.id, r.title, r.delivery_state); } ``` Dùng `limit=100` khi đọc số lượng lớn. Cách này giảm một nửa số request tính vào [rate limit](https://developer.hillsmusic.com/vi/guides/rate-limits/) của bạn. ### Trang và bản phát hành mới Trang được chia theo id bản phát hành. Một bản phát hành được tạo trong lúc bạn đang duyệt trang có id cao hơn những bản bạn đã đọc, nên nó chỉ xuất hiện ở một trang đầu mới. Bạn sẽ không bị trùng lặp hay bỏ sót vì điều đó. Một bản phát hành đổi trạng thái trong lúc bạn duyệt trang vẫn giữ nguyên vị trí. Nếu bạn lọc theo `status`, một bản phát hành có thể rời khỏi bộ lọc giữa hai trang. Để không bỏ sót gì, hãy đồng bộ theo thời gian. ## Đồng bộ thay đổi Để giữ bản sao danh mục của riêng bạn luôn cập nhật, đừng đọc toàn bộ danh sách mỗi lần. Hãy dùng `updated_since`: 1. Ở lần chạy đầu tiên, đọc mọi trang. Ghi lại thời điểm bắt đầu, theo UTC. 2. Ở lần chạy tiếp theo, gửi `updated_since` bằng thời điểm đó, và phân trang như bình thường. 3. Lưu thời điểm bắt đầu mới cho lần chạy sau. ```bash curl "https://api.hillsmusic.com/v1/releases?updated_since=2026-10-18T06:00:00Z&limit=100" \ -H "Authorization: Bearer $HILLS_API_KEY" ``` ```js const startedAt = new Date().toISOString(); for await (const r of allReleases({ updated_since: lastRun })) await upsert(r); lastRun = startedAt; // store it ``` `updated_since` khớp với các bản phát hành thay đổi tại hoặc sau thời điểm bạn gửi, nên một bản phát hành thay đổi đúng ở ranh giới sẽ được trả về hai lần. Hãy làm cho thao tác ghi của bạn idempotent theo `id` của bản phát hành. Dùng thời điểm bắt đầu thay vì thời điểm kết thúc nghĩa là một bản phát hành thay đổi trong lúc chạy sẽ được đọc lại ở lần sau. Với [webhook](https://developer.hillsmusic.com/vi/guides/webhooks/), bạn có thể phản ứng với thay đổi ngay khi nó xảy ra, và giữ `updated_since` làm lượt đồng bộ bù mỗi đêm. ## Lần gửi webhook: before `GET /webhooks/{id}/deliveries` trả về những gì đã được gửi đến một endpoint trong 30 ngày gần nhất, mới nhất trước. Phản hồi có `has_more`. Khi giá trị này là `true`, hãy gửi id của lần gửi cuối cùng trên trang trong `before`: ```bash curl "https://api.hillsmusic.com/v1/webhooks/7/deliveries?status=failed&limit=100" \ -H "Authorization: Bearer $HILLS_API_KEY" # { "data": [ …, { "id": 1001, … } ], "has_more": true } curl "https://api.hillsmusic.com/v1/webhooks/7/deliveries?status=failed&limit=100&before=1001" \ -H "Authorization: Bearer $HILLS_API_KEY" ``` ```js async function failedDeliveries(endpointId) { const out = []; let before = null; for (;;) { const url = new URL(`https://api.hillsmusic.com/v1/webhooks/${endpointId}/deliveries`); url.searchParams.set("status", "failed"); url.searchParams.set("limit", "100"); if (before) url.searchParams.set("before", String(before)); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.HILLS_API_KEY}` }, }); const page = await res.json(); out.push(...page.data); if (!page.has_more || !page.data.length) return out; before = page.data.at(-1).id; } } ``` Bạn có thể lọc theo `status` (`pending`, `succeeded` hoặc `failed`) và `event`. Giữ nguyên bộ lọc trên mọi trang. ## Lỗi Một cursor không phải do API cấp sẽ trả về `400 invalid` với `field: "cursor"`. Một `limit` nằm ngoài khoảng 1–100 không bị từ chối mà được đưa về trong khoảng, nên `limit=500` cho trang 100 phần tử, và một `limit` không phải số sẽ dùng giá trị mặc định 50. Xem [Lỗi](https://developer.hillsmusic.com/vi/guides/errors/). Nguồn: https://developer.hillsmusic.com/vi/guides/pagination/