Webhooks and signatures
Get delivery, reply, consent and suppression events pushed to your server, and verify each one is from Hermeticor.
What you need first
An HTTPS endpoint on a public host. Private and loopback addresses are refused.
A key with
integrations:write.
1. See the topics
curl -s "$BASE/v1/webhooks/topics" -H "Authorization: Bearer $HERMETICA_API_KEY"Useful ones: message.status (delivery changes), message.replied, consent.changed, suppression.added, domain.*, connection.health_changed, mailbox.paused. An unknown topic is refused with 400.
2. Create the endpoint
curl -s -X POST "$BASE/v1/webhooks/endpoints" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/hermetica","topics":["message.status","message.replied","consent.changed","suppression.added"],
"description":"Production"}'The answer is 201 with a secret. It is shown once. Store it. POST /v1/webhooks/endpoints/<id>/rotate-secret gives a new one.
3. Verify every request
Each request carries:
Hermetica-Signature: t=<unix seconds>,v1=<hex>Hermetica-Event-IdandHermetica-Topic
v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Check it against the raw body, before you parse JSON:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false; // reject old or replayed requests
const want = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return want.length === parts.v1.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}Answer with a 2xx quickly and do the work afterwards. Use Hermetica-Event-Id to ignore an event you already handled.
4. Retries
A delivery that fails or times out (10 seconds) is retried with backoff: 30 seconds, doubling up to 6 hours between tries, for 24 hours. After that it is marked failed. You can replay any delivery:
curl -s "$BASE/v1/webhooks/endpoints/<endpointId>/deliveries" -H "Authorization: Bearer $HERMETICA_API_KEY"
curl -s -X POST "$BASE/v1/webhooks/deliveries/<deliveryId>/replay" -H "Authorization: Bearer $HERMETICA_API_KEY"5. Test it
curl -s -X POST "$BASE/v1/webhooks/endpoints/<endpointId>/test" -H "Authorization: Bearer $HERMETICA_API_KEY"
curl -s -X POST "$BASE/v1/webhooks/endpoints/<endpointId>/test-topic" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-d '{"topic":"message.status"}'Filters
Only get email events:
curl -s -X POST "$BASE/v1/webhooks/endpoints/<endpointId>/filters" \
-H "Authorization: Bearer $HERMETICA_API_KEY" -H "Content-Type: application/json" \
-d '{"path":"channel","op":"eq","value":"email"}'Every filter must match. Topics without that field are then dropped.
History
GET /v1/webhooks/journal?limit=50 lists every event sent. GET /v1/webhooks/summary shows success rates per endpoint.