Subscribe an HTTPS endpoint and ReachHQ will POST a signed event the moment something happens on a campaign. Deliveries are HMAC-signed, retried with backoff, and dead-lettered if your endpoint stays down.
Register a webhook from the API (scope webhooks:write) or in the app. Pass the destination url and the events you want; an empty array subscribes to all event types. The response includes a signingSecret shown only once.
curl -X POST https://api.reachhq.io/api/v1/webhooks \
-H "Authorization: Bearer rhq_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/reachhq", "events": ["REPLIED", "BOUNCED"] }'
# { "id": "wh_...", "url": "...", "events": ["REPLIED","BOUNCED"],
# "enabled": true, "signingSecret": "..." } <- secret is returned ONCE| Event | Fires when | data fields |
|---|---|---|
SENT | An email was sent to a recipient. | recipientId, email, campaignId |
OPEN | A recipient opened a tracked email. | recipientId |
CLICK | A recipient clicked a tracked link. | recipientId, url |
REPLIED | A reply was received and attributed to the campaign. | recipientId, email |
BOUNCED | A bounce was detected for a recipient. | recipientId, email |
UNSUBSCRIBE | A recipient unsubscribed. | recipientId, email |
You can also fire a one-off TEST delivery (data: { "ok": true }) from a subscription to check your endpoint end to end.
Every delivery is a JSON POST with the same envelope, { event, data, timestamp }, and two headers: X-ReachHQ-Event (the event type) and X-ReachHQ-Signature (sha256=<hex>, an HMAC of the body).
POST /hooks/reachhq HTTP/1.1
Content-Type: application/json
X-ReachHQ-Event: REPLIED
X-ReachHQ-Signature: sha256=4f1a...c7
{
"event": "REPLIED",
"data": { "recipientId": "rcp_123", "email": "ada@example.com" },
"timestamp": "2026-06-19T12:00:00.000Z"
}Compute an HMAC-SHA256 of the raw request body using your signingSecret and compare it to the X-ReachHQ-Signature header with a constant-time comparison. Reject anything that does not match.
import { createHmac, timingSafeEqual } from "node:crypto";
// The signing secret returned when you created the webhook.
const SECRET = process.env.REACHHQ_WEBHOOK_SECRET;
// Verify against the EXACT raw request body (do not re-serialize the parsed JSON).
export function isValidSignature(rawBody, header) {
const expected = "sha256=" + createHmac("sha256", SECRET).update(rawBody).digest("hex");
const got = Buffer.from(header ?? "");
const want = Buffer.from(expected);
return got.length === want.length && timingSafeEqual(got, want);
}GET /api/v1/webhooks/deliveries/dlq and retry one with POST /api/v1/webhooks/deliveries/:id/requeue.