Documentazione
1. Pubblica 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"
}'La chiamata risponde non appena l'evento viene registrato e messo in coda. Se includi una dedupeKey, un eventuale tentativo di re-invio da parte del tuo job runner non invierà l'evento due volte.
2. Cosa riceve il tuo 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. Come verificarlo
Fornisci questo ai tuoi clienti. I due errori che fanno tutti sono confrontare con === e ignorare il timestamp: entrambi passano del tutto inosservati.
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)
})
}Effettua la verifica sul corpo grezzo, prima di qualsiasi parsing JSON. Una nuova serializzazione altera i byte e fa fallire ogni firma.
4. Ruotare un secret
La rotazione imposta un nuovo secret mantenendo valido il precedente per un intervallo a tua scelta. Firmiamo subito con quello nuovo per incentivare i tuoi clienti a migrare, ma i destinatari che verificano ancora con il vecchio secret continueranno a funzionare finché la finestra non si chiude.
5. Il log di consegna
Genera un token per il portale nel tuo backend — con la stessa struttura di una firma webhook, indicando uno dei tuoi clienti — e richiama l'API del portale dalla tua pagina delle impostazioni. Ogni richiesta è delimitata all'utente indicato in quel token, così nessun cliente potrà mai vedere i payload di un altro.
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>Oppure integralo
Sui piani Pro e Scale, inserisci <hookpulse-log> nella pagina delle impostazioni invece di creare una tabella personalizzata. Stesso token, stesso ambito: esegue il rendering del log per te all'interno di una shadow root, evitando qualsiasi conflitto con il CSS della tua pagina.
<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>Limiti trasparenti
- La coda è una tabella PostgreSQL. In questo modo il registro delle consegne e la coda non possono mai andare in disaccordo e un riavvio non fa perdere nulla. Non è l'architettura più veloce possibile, e con un throughput molto elevato un message broker risulterebbe migliore.
- Il worker viene eseguito nel processo web. Il sistema di acquisizione dei task è già sicuro per più worker, quindi spostarlo all'esterno è una modifica a livello di deployment piuttosto che una riscrittura — ma attualmente il carico delle consegne e quello delle richieste condividono lo stesso processo.
- Non seguiamo i reindirizzamenti. Seguirne uno significherebbe inviare un payload firmato a un host non specificato dall'utente. Un codice 3xx viene gestito come un errore.
- Un endpoint che fallisce per 20 volte consecutive viene disattivato. Un URL non funzionante non dovrebbe continuare a consumare tentativi a discapito di tutti gli altri.