Project API

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.

  1. 01
    Add an endpoint

    Go to Developers → Webhooks and add your HTTPS URL.

  2. 02
    Pick events

    Subscribe to specific events, or leave them all unchecked to receive everything.

  3. 03
    Save the signing secret

    It's shown once on creation — store it to verify incoming requests.

  4. 04
    Send 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:

POST body
{
  "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:

Headers
Email-Bump-Event: contact.created
Email-Bump-Signature: t=1784663798,v1=acf8fe8e05a9fde3347ad708ac3640c9…

Event types

  • contact.created, contact.updated, contact.deleted
  • contact.subscribed, contact.unsubscribed
  • email.delivered, email.opened, email.clicked
  • email.bounced, email.complained
  • email.received — inbound mail arriving
  • campaign.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:

Node.js
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);
}
Use the raw request body

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.