Paginación
Recorre los lanzamientos con cursor y next_cursor, las entregas de webhooks con before y has_more, y sincroniza solo lo que ha cambiado.
Actualizado el
La mayoría de las listas de la API son cortas y llegan completas. Dos listas pueden ser largas y se paginan: los lanzamientos y las entregas de webhooks.
Qué listas se paginan
| Endpoint | Cómo se pagina | Tamaño de página |
|---|---|---|
GET /releases |
cursor → next_cursor |
limit 1–100, predeterminado 50 |
GET /webhooks/{id}/deliveries |
before → has_more |
limit 1–100, predeterminado 50 |
GET /ddex |
No se pagina. Tus últimas 100 entregas. | 100 |
GET /members |
No se pagina. Todos los artistas y sellos. | Todos |
GET /links |
No se pagina. Todos los smart links y pre-saves. | Todos |
GET /payouts |
No se pagina. Todas las solicitudes de retiro. | Todas |
GET /webhooks |
No se pagina. Como máximo 20 endpoints. | Todos |
GET /earnings |
No se pagina. Agrupado según by durante months (1–60). |
Todo |
Lanzamientos: cursores
GET /releases devuelve los lanzamientos del más reciente al más antiguo. Cada respuesta incluye next_cursor. Devuélvelo como cursor para obtener la página siguiente. En la última página, next_cursor es null.
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"
Trata el cursor como una cadena opaca. No lo construyas ni lo modifiques tú.
Mantén los demás parámetros iguales en todas las páginas. Si filtras la primera página con status=approved, envía también status=approved con el cursor. Si cambias un filtro, empieza de nuevo sin cursor.
Todos los lanzamientos, en Node
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);
}
Usa limit=100 para las lecturas masivas. Reduce a la mitad el número de solicitudes que consumen tu límite de solicitudes.
Páginas y lanzamientos nuevos
Las páginas se cortan por id de lanzamiento. Un lanzamiento creado mientras paginas tiene un id mayor que los que ya has leído, así que solo aparece en una nueva primera página. No te provoca duplicados ni huecos.
Un lanzamiento que cambia de estado mientras paginas se queda en su sitio. Si filtras por status, un lanzamiento puede salir del filtro entre una página y otra. Para no perderte nada, sincroniza por fecha.
Sincronizar cambios
Para mantener al día tu propia copia del catálogo, no leas la lista completa cada vez. Usa updated_since:
- En la primera ejecución, lee todas las páginas. Anota la hora a la que empezaste, en UTC.
- En la siguiente ejecución, envía
updated_sincecon esa hora y pagina como de costumbre. - Guarda la nueva hora de inicio para la ejecución posterior.
curl "https://api.hillsmusic.com/v1/releases?updated_since=2026-10-18T06:00:00Z&limit=100" \
-H "Authorization: Bearer $HILLS_API_KEY"
const startedAt = new Date().toISOString();
for await (const r of allReleases({ updated_since: lastRun })) await upsert(r);
lastRun = startedAt; // store it
updated_since devuelve los lanzamientos modificados en la hora que envías o después, así que un lanzamiento modificado justo en el límite llega dos veces. Haz que tus escrituras sean idempotentes por el id del lanzamiento.
Usar la hora de inicio, y no la de fin, hace que un lanzamiento que cambia durante la ejecución se vuelva a leer la próxima vez. Con los webhooks puedes reaccionar a los cambios en cuanto se producen y dejar updated_since como repaso nocturno.
Entregas de webhooks: before
GET /webhooks/{id}/deliveries devuelve lo que se envió a un endpoint en los últimos 30 días, de lo más reciente a lo más antiguo. Responde con has_more. Cuando es true, pasa como before el id de la última entrega de la página:
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"
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;
}
}
Puedes filtrar por status (pending, succeeded o failed) y por event. Mantén los mismos filtros en todas las páginas.
Errores
Un cursor que no te haya dado la API responde 400 invalid con field: "cursor". Un limit fuera de 1–100 no se rechaza: se ajusta al intervalo, de modo que limit=500 da páginas de 100, y un limit que no es un número da el valor predeterminado de 50. Consulta Errores.