Documentación
1. Publicar un evento
curl -X POST https://hookpulse.altixcode.com/api/v1/apps/APP_ID/events \
-H "Authorization: Bearer hp_…" \
-H "Content-Type: application/json" \
-d '{
"type": "invoice.paid",
"payload": { "id": "in_123", "amount": 4900 },
"dedupeKey": "invoice-123-paid"
}'Responde tan pronto como el evento se registra y se pone en cola. Pasa un dedupeKey y un reintento de tu ejecutor de tareas no enviará el evento dos veces.
2. Qué recibe tu cliente
POST /their/endpoint
Content-Type: application/json
HookPulse-Signature: t=1700000000,v1=<base64 hmac>
HookPulse-Delivery-Id: dlv_…
HookPulse-Timestamp: 1700000000
{"id":"evt_…","type":"invoice.paid","createdAt":"…","data":{…}}3. Verificación
Comparte esto con tus clientes. Los dos errores que todo el mundo comete son comparar con === e ignorar la marca de tiempo, y ambos fallan en silencio.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(headers, rawBody, secret, toleranceSeconds = 300) {
const header = headers['hookpulse-signature']
const deliveryId = headers['hookpulse-delivery-id']
const timestamp = Number(/t=(\d+)/.exec(header)?.[1])
// Replay protection. Without this a captured request is valid forever.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = createHmac('sha256', secret)
.update(`${deliveryId}.${timestamp}.${rawBody}`)
.digest('base64')
return [...header.matchAll(/v1=([^,]+)/g)].some(([, presented]) => {
const a = Buffer.from(presented)
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
})
}Verifica contra el cuerpo raw (en bruto), antes de cualquier parseo de JSON. Volver a serializar altera los bytes e invalida cualquier firma.
4. Rotar un secreto
Rotar establece un nuevo secreto y mantiene válido el anterior durante el margen de tiempo que elijas. Firmamos con el nuevo de inmediato para incentivar la migración de tus clientes, pero un receptor que todavía verifique con el secreto anterior seguirá funcionando hasta que concluya ese margen.
5. El registro de entregas
Genera un token de portal en tu backend —con la misma estructura que la firma de un webhook, identificando a uno de tus clientes— y llama a la API del portal desde tu propia página de configuración. Cada consulta está restringida al suscriptor indicado en ese token, por lo que un cliente jamás podrá ver las cargas útiles de otro.
GET https://hookpulse.altixcode.com/api/portal/APP_ID/deliveries
POST https://hookpulse.altixcode.com/api/portal/APP_ID/deliveries/DELIVERY_ID/retry
Authorization: Bearer <portal token>O incrústalo
En los planes Pro y Scale, añade <hookpulse-log> en tu página de configuración en lugar de crear tu propia tabla. Mismo token, mismo alcance: simplemente renderiza el registro por ti en un shadow root para evitar conflictos con el CSS de tu página.
<script src="https://hookpulse.altixcode.com/embed/hookpulse-log.js"></script>
<hookpulse-log app-id="APP_ID" token="PORTAL_TOKEN" api-url="https://hookpulse.altixcode.com"></hookpulse-log>Límites claros y honestos
- La cola es una tabla de PostgreSQL. Esto hace imposible que el registro de entregas y la cola discrepen, y garantiza que un reinicio no pierda nada. No es el diseño más rápido posible, y con un volumen muy alto un broker ganaría.
- El worker se ejecuta en el proceso web. La asignación ya es segura para varios workers, por lo que separarlo es un cambio de despliegue y no una reescritura; sin embargo, hoy la carga de entregas y la de peticiones comparten proceso.
- No seguimos redirecciones. Hacerlo enviaría una carga útil firmada a un host que nunca especificaste. Las respuestas 3xx se consideran un fallo.
- Un endpoint que falla 20 veces consecutivas se desactiva. Una URL inactiva no debería seguir consumiendo intentos a expensas de los demás.