Guía

Eventos durables y webhooks firmados

Cada cambio relevante produce un evento con objetivo de retención de 90 días; la eliminación TTL es eventual. Los webhooks se entregan al menos una vez: deduplica siempre.

Suscribirse

Registra hasta 20 URLs públicas HTTPS en el puerto 443 y hasta 30 tipos por suscripción. No se aceptan direcciones privadas, loopback, link-local ni destinos que resuelvan hacia ellas. Usa tipos específicos o *. El secreto whsec_… se muestra al crear o rotar y la rotación entra en vigor inmediatamente.

Verificar una entrega

Moku envía Moku-Event-Id, Moku-Delivery-Id y Moku-Signature: t=<unix>,v1=<hex>. Calcula HMAC-SHA256 con el secreto completo sobre timestamp + "." + cuerpo_crudo, compara en tiempo constante y rechaza timestamps antiguos.

Verifica la firma sobre el cuerpo crudo recibido, antes de JSON.parse. Este módulo para Node.js 18 o posterior compara en tiempo constante y rechaza firmas con más de 5 minutos de diferencia. Responde 401 cuando devuelva false.

import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;

export function verifyMokuSignature(rawBody, signatureHeader, signingSecret, now = Date.now()) {
  const fields = Object.fromEntries(
    String(signatureHeader ?? '').split(',').map(part => part.trim().split('=', 2)),
  );
  const timestamp = Number(fields.t);
  if (!Number.isSafeInteger(timestamp) || !/^[0-9a-f]{64}$/.test(fields.v1 ?? '')) return false;
  if (Math.abs(now / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = createHmac('sha256', signingSecret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(fields.v1, 'hex'));
}

El cuerpo del evento contiene metadatos del recurso, no su snapshot completo; consulta el endpoint del recurso para obtener el estado actual.

Entrega y reintentos

Reproducir y consultar el resultado

POST /vendors/{vendor_id}/events/{event_id}/replay responde 202 Accepted cuando el trabajo quedó guardado, no cuando terminó la entrega. Guarda replay_id y consulta:

GET /vendors/{vendor_id}/events/{event_id}/replays/{replay_id}

Necesitas webhooks.write para iniciar y webhooks.read para consultar. El estado avanza entre queued, running, completed o failed. total es null hasta seleccionar los destinos. Los contadores delivered, failed y skipped permiten distinguir resultados parciales; una suscripción eliminada o desactivada se omite.

Cada POST crea una reproducción nueva. Si pierdes la respuesta, no asumas que el primer intento falló: repetirlo puede duplicar entregas.

Leer eventos sin perder ninguno

La lista de eventos recorre todos los eventos confirmados de la tienda en un solo orden de llegada. La primera lectura que ve un evento lo numera después de todos los que ya entregó, así que continuar desde el cursor nunca salta un evento, aunque se haya confirmado tarde.

Es el orden en que Moku descubre los eventos, no el de confirmación ni el de occurred_at. Para ordenar cambios de un mismo recurso, usa resource.version.

GET /vendors/{vendor_id}/events?limit=100&page_after={next_cursor}

Guarda siempre page.next_cursor: viene en todas las respuestas, también en las vacías. Si page.has_more es true, pide la página siguiente de inmediato. Si es false, ya estás al día: vuelve a consultar más tarde con ese mismo cursor.

La lectura es al menos una vez y una página puede repetir un evento. Deduplica por id.

GET /vendors/{vendor_id}/events?occurred_after=2026-10-06T12:00:00Z

occurred_after, una fecha y hora RFC 3339 con zona horaria, devuelve solo eventos con occurred_at posterior. Filtra ese orden sin cambiarlo, y una página puede avanzar el cursor sin devolver eventos. El cursor queda ligado al filtro: si cambias occurred_after, empieza de nuevo sin page_after.

Tipos publicados en v1