Webhooks
Recibe eventos de HILLS Music en tu endpoint HTTPS: suscripción, los 13 tipos de evento, el sobre, la firma X-Hills-Signature, reintentos y registro.
Actualizado el
Los webhooks avisan a tu sistema cuando ocurre algo en HILLS Music, para que no tengas que sondear. HILLS envía un POST firmado a tu endpoint HTTPS por cada evento al que te suscribas. Los webhooks se incluyen con la API en los planes Enterprise y Custom.
Suscríbete
Crea un endpoint con POST /webhooks (ámbito webhooks:write). También puedes añadirlo en la aplicación web de HILLS, en Settings → Webhooks.
curl -X POST "https://api.hillsmusic.com/v1/webhooks" \
-H "Authorization: Bearer $HILLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hills/webhooks","description":"Order system","events":["release.approved","delivery.store_status_changed"]}'
La respuesta (201) contiene el endpoint y su secreto de firma, whsec_…. El secreto se muestra una sola vez. Guárdalo con tus demás secretos.
La URL debe ser https:// en el puerto 443 y con una dirección pública. Las direcciones privadas, de loopback, de enlace local y de metadatos de la nube se rechazan con 400 invalid_url, tanto al guardar la URL como en cada solicitud. Puedes tener hasta 20 endpoints (a partir de ahí, 409 too_many).
Todas las solicitudes de endpoints de webhook, con sus campos y errores, están en la referencia de Webhooks. GET /webhooks/events enumera los tipos de evento con el significado de cada uno.
Eventos
| Evento | Se envía cuando |
|---|---|
release.submitted |
Se envió un lanzamiento a revisión. |
release.approved |
Se aprobó un lanzamiento y está en cola para sus plataformas. |
release.changes_requested |
Se devolvió un lanzamiento con lo que hay que corregir. |
release.live |
Un lanzamiento se publicó en su primera plataforma. |
delivery.store_status_changed |
Cambió el estado de un lanzamiento en una plataforma: en camino, enviado, publicado, necesita tu corrección, retirada solicitada o retirado. |
takedown.completed |
Todas las plataformas de una solicitud de retirada aprobada han retirado el lanzamiento. |
ddex.import.report_ready |
Terminó una prueba DDEX (la carpeta /test, la aplicación web o POST /ddex/validate); su informe está listo. |
ddex.import.imported |
Terminó la importación de un lote DDEX entregado a producción: done, partial o failed. |
statement.published |
La liquidación de ingresos de un mes está lista (en marca blanca, primero lista para que la revises). |
payout.status_changed |
Una solicitud de pago se aprobó, se envió, se pagó, falló o se canceló. |
request.status_changed |
HILLS Music respondió a una solicitud: aprobada, necesita cambios, completada o rechazada. |
chat.message |
HILLS Music te escribió en el soporte en vivo. |
webhook.test |
Solo lo envía POST /webhooks/{id}/test. No puedes suscribirte a él. |
El sobre
Todos los eventos tienen la misma forma exterior:
{
"id": "5d0c2b7e-8a43-4c4e-9f1e-2f0b9a6c1d33",
"type": "release.approved",
"api_version": "v1",
"occurred_at": "2026-10-13T09:05:00Z",
"account_id": 12,
"data": { "release": { "id": 812, "title": "Midnight Run" } }
}
| Campo | Significado |
|---|---|
id |
El UUID del evento. Es el mismo en todos los intentos: úsalo para descartar duplicados. |
type |
El tipo de evento. |
api_version |
v1. Dentro de una versión, los campos solo se añaden. |
occurred_at |
Cuándo ocurrió, en ISO 8601 y en UTC. |
account_id |
Tu cuenta de socio. |
data |
Los datos del evento. Su forma depende de type. |
Cada solicitud lleva además X-Hills-Signature, X-Hills-Event (el tipo), X-Hills-Delivery (el id de la entrega, el mismo en todos los intentos), X-Hills-Attempt (1, 2, 3…), Content-Type: application/json y User-Agent: HILLS-Webhooks/1.0.
Un cuerpo ocupa como máximo 256 KB. Un evento más grande se envía con data.truncated: true y solo sus ids. Lee el resto en la API.
No se garantiza que los eventos lleguen en orden. Usa occurred_at y, cuando el orden importe, lee el estado actual en la API.
Verifica la firma
X-Hills-Signature tiene este aspecto: t=1792141500,v1=5257a869…. Para comprobarla:
- Lee
t(segundos Unix) yv1de la cabecera. - Rechaza la solicitud si
tse aleja más de 5 minutos de tu reloj. Así se evitan los ataques de repetición. - Calcula el HMAC-SHA256 de
<t>.<raw body>con tu secreto tal cual como clave (incluidowhsec_). Codifícalo en hexadecimal. - Compáralo con
v1en tiempo constante.
Usa los bytes del cuerpo sin procesar. Un JSON analizado y vuelto a serializar no coincidirá.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.HILLS_WEBHOOK_SECRET; // whsec_…
const app = express();
app.post("/hills/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Hills-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=", 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// Dedupe on event.id: a retry after a lost answer sends the same event again.
queue.push(event); // do the work later
res.sendStatus(200); // answer within 10 seconds
});
app.listen(3000);
Responde rápido
Cualquier 2xx en menos de 10 segundos es un éxito. Todo lo demás es un fallo, incluidos un tiempo de espera agotado o un 3xx (no se siguen las redirecciones). Responde primero y haz después el trabajo en una cola.
Reintentos
Una entrega fallida se vuelve a intentar, hasta 8 intentos en total. Tras cada fallo, el siguiente intento llega 1 minuto, 5 minutos, 30 minutos, 2 horas, 12 horas, 12 horas y 12 horas después: en total, aproximadamente día y medio. Tras el 8.º fallo la entrega se abandona, pero todavía puedes reintentarla a mano.
Desactivación automática
Un endpoint que falla 20 intentos seguidos, sin ningún éxito entre medias, se desactiva. Recibes una notificación. Las entregas pendientes en ese momento se abandonan.
GET /webhooks/{id} muestra el motivo: enabled: false, disabled_at y disabled_reason. Corrige tu endpoint y vuelve a activarlo:
curl -X PATCH "https://api.hillsmusic.com/v1/webhooks/7" \
-H "Authorization: Bearer $HILLS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true}'
Eso pone a cero failure_count. Después, reintenta lo que se abandonó.
El registro de entregas
GET /webhooks/{id}/deliveries enumera lo que se envió en los últimos 30 días, de lo más reciente a lo más antiguo: el payload, los intentos, el último estado HTTP, la hora y el error. Filtra con status (pending, succeeded, failed) y event. Se pagina con before. Consulta Paginación.
Para volver a enviar una entrega, con el mismo id de evento y el mismo payload:
curl -X POST "https://api.hillsmusic.com/v1/webhooks/7/deliveries/1042/retry" \
-H "Authorization: Bearer $HILLS_API_KEY"
Mientras el endpoint esté desactivado, el reintento y la prueba responden 409 endpoint_disabled.
Renueva el secreto
POST /webhooks/{id}/roll-secret genera un secreto nuevo. La respuesta lo incluye, una sola vez. El secreto anterior deja de funcionar al instante, así que planifica el cambio:
- Haz que tu verificador acepte una lista de secretos y pruebe cada uno por turno.
- Renueva el secreto y añade el nuevo a la lista.
- Cuando las entregas se validen con el secreto nuevo, quita el anterior.
Renuévalo de inmediato si el secreto se filtra.
Envía un evento de prueba
POST /webhooks/{id}/test pone en cola un evento webhook.test para el endpoint, esté suscrito a los eventos que esté. Se firma como cualquier otro evento y su resultado aparece en el registro de entregas.
curl -X POST "https://api.hillsmusic.com/v1/webhooks/7/test" \
-H "Authorization: Bearer $HILLS_API_KEY"
const res = await fetch("https://api.hillsmusic.com/v1/webhooks/7/test", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.HILLS_API_KEY}` },
});
const { delivery } = await res.json(); // 202; check GET /webhooks/7/deliveries for the outcome
Para hacer pruebas en tu propio equipo, usa un túnel que te dé una URL HTTPS pública.