---
title: Webhooks
description: Recibí pulsos y eventos de auditoría en tu propio endpoint.
---

Hay dos formas de recibir empujes de Moodinary. Ambas se configuran desde el
panel, en la pantalla de **Integraciones** de tu organización, y requieren el
plan **Insight** (o superior).

## Notificaciones (Slack, Discord, Teams)

El resumen semanal llega directo a tu canal: pegás la URL del webhook de tu
servicio, elegís el destino y listo. Sin código.

## Probar tu configuración

Desde el panel (**Integraciones → Webhooks**) podés disparar un evento de
prueba a cada destino sin esperar al próximo pulso real. Si no llega,
revisá que tu endpoint responda `2xx` en menos de 5 segundos y que el
secreto configurado coincida. El secreto se muestra una sola vez al crearlo
o regenerarlo: guardalo en tu servidor. No vuelve a aparecer al abrir el
panel. Si lo perdiste, regeneralo y actualizá tu receptor; las firmas
anteriores dejan de validar con el nuevo secreto.

## Webhooks de datos (tu endpoint)

Pulsos al momento de recibirse y eventos del registro de auditoría,
entregados en crudo a una URL tuya, para que los proceses como quieras.
Podés prender cada tipo por separado y dejar la URL vacía para desactivar.

### Verificación

Cada envío lleva un identificador estable y un timestamp. Para receptores
nuevos, verificá **X-Moodinary-Signature-V2** antes de confiar en el payload:
es el HMAC-SHA256 de `timestamp + "." + deliveryId + "." + bodyCrudo`.
El body se verifica exactamente como llegó, sin volver a serializarlo.

```http
X-Moodinary-Event: pulse.created
X-Moodinary-Delivery-Id: <id>
X-Moodinary-Timestamp: <segundos Unix>
X-Moodinary-Signature-V2: sha256=<hex>
X-Moodinary-Signature: sha256=<hex del body crudo, contrato anterior>
```

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function isValid(rawBody, headers, secret, now = Date.now()) {
  const timestamp = headers.get("X-Moodinary-Timestamp") ?? "";
  const id = headers.get("X-Moodinary-Delivery-Id") ?? "";
  const signature = headers.get("X-Moodinary-Signature-V2") ?? "";
  if (!/^\d{1,12}$/.test(timestamp) || !id || id.length > 200) return false;
  const age = now / 1000 - Number(timestamp);
  if (age > 300 || age < -30) return false;
  if (!/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${id}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

Después de verificar, tu receptor debe guardar el ID y encolar el trabajo
**en una operación atómica**. Un ID ya procesado debe responder `2xx` sin
repetir sus efectos. Conservá los IDs al menos durante la ventana de
aceptación del timestamp (por ejemplo, diez minutos). Comprobá también que
el `id` del JSON coincida con el header. La firma permite detectar cambios;
la ventana temporal y la deduplicación las implementa **tu receptor**.
Moodinary no puede imponerlas por vos. El header anterior se conserva por
compatibilidad, pero por sí solo no protege contra replay.

:::caution\[Entrega con reintentos acotados]
Cada envío puede hacer hasta tres intentos, con timeout de 5 segundos por
intento y pausas de 250 ms y 1 segundo. Se reintentan errores de conexión,
timeouts y respuestas `408`, `429` o `5xx`. Los demás errores, incluidos
redirects, no se reintentan. El body, ID, timestamp y firmas no cambian
entre intentos. Respondé `2xx` rápido y procesá después.

No hay cola durable ni garantía de entrega: si se agotan los intentos o
se interrumpe la ejecución, el envío puede perderse. Un timeout también
puede ocurrir después de que tu servidor lo haya aceptado, así que
podés recibir duplicados. Los logs operativos registran solo ID, tipo de
canal, número de intento, estado HTTP y resultado; no guardan el body,
la URL, el secreto ni la respuesta. No son un historial de entregas en
el panel.
:::

Solo se aceptan destinos HTTPS públicos sin credenciales en la URL. No
se siguen redirects. La validación del hostname no elimina por sí sola
el riesgo de DNS rebinding.

El resumen semanal congela su contenido y registra un checkpoint por
canal en el workflow de producción. Un canal ya confirmado no vuelve a
enviarse porque otro falle. Las alertas se marcan enviadas después de
confirmar todos los canales habilitados. Incluye todas las alertas
pendientes, sin el anterior tope de 50; siguen aplicando los límites de
mensaje del servicio de chat. Las suscripciones canceladas conservan el
resumen hasta el fin del período pagado. Una caída entre aceptación y
checkpoint, o una nueva corrida manual, todavía puede producir duplicados.

### `pulse.created`

Se dispara al momento de recibirse cada pulso, en paralelo con la
moderación: `moderationStatus` es el valor **pre-moderación**, no el final.

```json
{
  "event": "pulse.created",
  "id": "delivery_abc",
  "orgId": "org_123",
  "sentAt": "2026-08-19T09:12:00.500Z",
  "data": {
    "id": "pls_abc",
    "level": 4,
    "reason": "TRABAJO",
    "moderationStatus": "CLEAN",
    "subteamId": "sub_1",
    "subteamName": "Ventas",
    "createdAt": "2026-08-19T09:12:00.000Z"
  }
}
```

### `audit.event`

Se dispara con cada entrada nueva del registro de auditoría (invitaciones,
cambios de rol, exportaciones…).

```json
{
  "event": "audit.event",
  "id": "delivery_def",
  "orgId": "org_123",
  "sentAt": "2026-08-19T09:12:00.500Z",
  "data": {
    "action": "member.invited",
    "actorUserId": "user_abc",
    "targetType": "member",
    "targetId": "juan@empresa.com",
    "metadata": null,
    "createdAt": "2026-08-19T09:12:00.000Z"
  }
}
```