Project API

Webhooks

Receive account events at your URL and verify their signatures.

View as Markdown ↗

How webhooks work

Webhooks push account events to your server the moment they happen — no polling. Email Bump sends a signed POST with a JSON body and retries with backoff until your endpoint returns a 2xx. Every attempt is logged so you can debug delivery.

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

  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": "[email protected]",
      "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.receivedinbound mail arriving
  • campaign.sent

That's the complete list — subscribing to none means you receive all of them.

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) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}
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.