Webhooks
Receive account events at your URL and verify their signatures.
View as Markdown ↗How webhooks work
Webhooks push events for the selected project to your server — no polling. Email Bump sends a signed POST with a JSON body and stops when your endpoint returns a 2xx. Failed deliveries get up to five attempts total, with delays of 5, 10, 20, and 40 seconds between attempts. Delivery records show the attempt count and latest outcome. Destinations refused by the public-address safety check are not retried.
A webhook URL has to resolve to a public address. Anything pointing at localhost, a private range, or a cloud metadata address is refused when you save it and again when a delivery is attempted — a name that resolves somewhere private later doesn't get a second chance. Redirects aren't followed, so the URL you give us is the only place we send.
Subscribe to events
Configure endpoints in the dashboard; there is currently no public /v1/webhooks management endpoint.
- 01Add an endpoint
Go to Developers → Webhooks and add your HTTPS URL.
- 02Pick events
Subscribe to specific events, or leave them all unchecked to receive everything.
- 03Save the signing secret
It's shown once on creation — store it to verify incoming requests.
- 04Send a test
Use Send test event and watch it appear under the endpoint's deliveries.
Payload shape
Every delivery is a JSON envelope with the event type and its data:
{
"id": "dfe336e3-9730-4eb4-8841-7b0bbd766dda",
"type": "contact.created",
"created": "2026-07-21T19:56:38Z",
"data": {
"contact": {
"id": "5fef0867-b292-46ca-8371-e24d8575576c",
"email": "jamie@example.com",
"subscribed": true
}
}
}Two headers accompany every request:
Email-Bump-Event: contact.created
Email-Bump-Signature: t=1784663798,v1=acf8fe8e05a9fde3347ad708ac3640c9…Event types
contact.created,contact.updated,contact.deletedcontact.subscribed,contact.unsubscribedemail.delivered,email.opened,email.clickedemail.bounced,email.complainedemail.received— inbound mail arrivingcampaign.sent
Subscribing to none means you receive all of these project events. The dashboard test button sends webhook.test separately.
Verify signatures
The Email-Bump-Signature header is t=<timestamp>,v1=<hmac>, where the HMAC is a SHA-256 of {timestamp}.{raw_body} keyed with your endpoint secret. Recompute it and compare before trusting a request:
import crypto from "node:crypto";
function verify(rawBody, header, secret, now = Date.now(), maxAgeSeconds = 300) {
if (!Buffer.isBuffer(rawBody) || typeof header !== "string" ||
typeof secret !== "string" || secret.length === 0 ||
!Number.isFinite(now) || !Number.isFinite(maxAgeSeconds) ||
maxAgeSeconds < 0) return false;
const match = /^t=([0-9]+),v1=([0-9a-f]{64})$/.exec(header);
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) ||
Math.abs(now / 1000 - timestamp) > maxAgeSeconds) return false;
const expected = crypto.createHmac("sha256", secret)
.update(match[1] + ".")
.update(rawBody)
.digest();
const supplied = Buffer.from(match[2], "hex");
return crypto.timingSafeEqual(expected, supplied);
}Verify against the exact bytes you received, before any JSON re-serialization — re-encoding can change the body and break the signature.
Pass a raw Node.js Buffer to the verifier and return a non-2xx for invalid signatures. It rejects malformed headers and timestamps more than five minutes from your clock. The timestamp is set when the event is queued and reused on retries, not refreshed per attempt: allow for clock skew and your recovery window, and investigate delayed valid events instead of disabling verification. See Node's constant-time comparison requirements.
After verification, durably record the envelope id with a unique constraint and process it idempotently. A valid signature does not prevent replay or duplicate delivery. Return 2xx only once safely accepted; do not assume arrival order or guaranteed delivery after retries are exhausted. Flow webhook actions are separate and do not automatically include these event-webhook signature headers.