Gareth Wilson Gareth Wilson

Guide to BigMailer Webhooks: Features and Best Practices

Published


BigMailer is an email marketing platform built on Amazon SES for teams that run several brands from one account, such as agencies or companies with multiple product lines. Its webhooks report what happens after a campaign goes out: a message bounces, a recipient marks it as spam, opens it, clicks a link or unsubscribes. Those five events are what you need to keep a CRM's suppression list accurate and to feed engagement data into your own reporting.

BigMailer's webhook documentation is short: a page of event types and a page on signatures. This guide covers both, and is clear about what BigMailer leaves unsaid.

What are BigMailer webhooks?

A BigMailer webhook endpoint belongs to the account, not to a brand. Every endpoint receives events for all of the account's brands, and each payload carries a brand_id so you can tell them apart.

BigMailer signs every request. Its scheme follows the same pattern as Stripe's: an X-BigMailer-Signature header holds a timestamp t and one or more v1 signatures, and each signature is an HMAC-SHA256 of the timestamp, a full stop and the raw body. Each endpoint has its own secret.

BigMailer webhook features

FeatureDetails
ConfigurationWebhooks settings in the BigMailer console
ScopeAll brands in the account, identified by brand_id
Signaturehex HMAC-SHA256
Signature headerx-bigmailer-signature
Header formatt=<unix>,v1=<signature>, possibly with more than one v1
Signed content<t>.<raw body>
SecretOne per endpoint, revealed with show
BodyJSON
Event routingBody field type
Retries, timeout, success codesNot documented

Events and payloads

EventTriggered WhenCommon Use Cases
bounceA message bounces (bounce.type is hard or soft)Suppress hard-bounced addresses, track deliverability
clickA recipient clicks a tracked linkEngagement scoring, link analytics
complaintA recipient marks a message as spamSuppress the contact, alert on complaint rates
openA recipient opens a messageEngagement tracking
unsubscribeA recipient unsubscribesSync opt-outs to your CRM or database

Each payload has type, brand_id, and one object named after the type. That object always holds date (Unix seconds), campaign_id, contact_id and email. click adds link_id and url. unsubscribe adds unsubscribe_all and unsubscribe_ids, the message types the contact opted out of. Here is BigMailer's bounce example, from the webhook samples library:

{
  "type": "bounce",
  "brand_id": "0da934ea-f96f-4f2f-85dd-0e8ceb9cba0e",
  "bounce": {
    "date": 1579811821,
    "campaign_id": "9ebe0652-6364-4688-897c-cf2655f988e8",
    "contact_id": "f36999a4-7fc0-4a03-868c-21fad154db1b",
    "email": "test@example.com",
    "type": "hard"
  }
}

The bounce object has its own type, hard or soft, which is the kind of bounce and not the event. A soft bounce is temporary, so most senders suppress an address only after a hard bounce.

Setting up BigMailer webhooks

  1. In the BigMailer console, open the Webhooks settings and add an endpoint with your HTTPS URL.
  2. Select the endpoint and click show to reveal its signing secret. Store it on your server, for example as BIGMAILER_WEBHOOK_SECRET. If you add more endpoints, each one has a different secret.
  3. Send a campaign to a test list and open it or click a link to produce events.

To receive through Hookdeck, create a source with the BigMailer source type, paste the endpoint's secret into its verification settings, and add the Hookdeck source URL as the endpoint in BigMailer. The source accepts POST. You can also forward deliveries to a local server with the Hookdeck CLI (npx hookdeck-cli listen 3000 bigmailer --path /webhooks/bigmailer).

Securing BigMailer webhooks

Split the header on commas. Take the single t= value as text and every v1= value. Build the message from the t text, a . and the raw body bytes, compute an HMAC-SHA256 with the endpoint secret, and accept the request if any v1 matches in constant time. BigMailer tells you to ignore schemes other than v1, to stop a downgrade to a weaker scheme. Its docs show the signature as 64 hex characters without naming the encoding, and the verifier below follows the example. The steps are in BigMailer's signature documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-bigmailer-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const parts = signature.split(',').map((part) => part.trim());
  const times = parts.filter((part) => part.startsWith('t=')).map((part) => part.slice(2));
  if (times.length !== 1 || !/^[0-9]+$/.test(times[0])) return false;
  const message = Buffer.concat([Buffer.from(`${times[0]}.`, 'utf8'), rawBody]);
  const candidates = parts.filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Once the signature matches, the event's details sit under the key named by type:

const express = require('express');

const app = express();

app.post('/webhooks/bigmailer', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.BIGMAILER_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
  try {
    const detail = event[event.type] || {};

    if (event.type === 'complaint' || (event.type === 'bounce' && detail.type === 'hard')) {
      await suppressContact(event.brand_id, detail.contact_id, detail.email);
    } else if (event.type === 'unsubscribe') {
      await recordOptOut(event.brand_id, detail.contact_id, detail.unsubscribe_all, detail.unsubscribe_ids);
    } else if (event.type === 'open' || event.type === 'click') {
      await recordEngagement(event.brand_id, detail);
    }
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

suppressContact, recordOptOut and recordEngagement are your own functions.

BigMailer suggests checking how old t is, but leaves the tolerance to you. The sample above doesn't check it. If you add a check to your own receiver, make it optional and generous: Hookdeck's BigMailer source verifies the same signature with no timestamp tolerance, and its retries and replays keep the original t, which a short window would reject. Hookdeck answers BigMailer with 200 and records any verification failure on the request.

Make BigMailer webhooks production-ready. Hookdeck Event Gateway checks every X-BigMailer-Signature, keeps a log of each bounce, complaint and unsubscribe, and retries delivery to your service when it fails.

BigMailer webhook limitations and pain points

No documented delivery guarantees

The Problem: You can't tell whether a bounce or unsubscribe that hit a failing endpoint will ever be sent again.

Why It Happens: BigMailer's webhook pages state no retry schedule, no response timeout and no rule for which status codes count as success.

Workarounds:

  • Return a 2xx quickly and keep the endpoint highly available.
  • Treat BigMailer's own contact data as the source of truth for suppression, and reconcile against it periodically.

How Hookdeck Can Help: Hookdeck accepts and stores each event as soon as it arrives, then retries your service on a schedule you set, so the gaps in BigMailer's documentation stop mattering for anything after the first hop.

No event ID

The Problem: A duplicate delivery looks exactly like a new event.

Why It Happens: Payloads carry no event or delivery ID.

Workarounds:

  • Build a key from type, contact_id, campaign_id and date, and store it when you process an event.
  • Make suppression and opt-out writes idempotent, so a repeat changes nothing.

How Hookdeck Can Help: Hookdeck's deduplication can compare those fields and drop repeats before they reach your service.

An unnamed encoding and a Stripe-style header

The Problem: A verifier that works for one request fails on another.

Why It Happens: BigMailer's header can hold several v1 values and other schemes, and the docs never say the digest is hex. Code that reads only the first signature, or that compares the whole header, breaks as soon as the header has more than one entry.

Workarounds:

  • Parse every element of the header, keep the one t and all v1 values, and accept a match on any of them.
  • Check each candidate is 64 hex characters before decoding it.

How Hookdeck Can Help: Hookdeck's BigMailer source parses the header and checks every v1 value for you.

One endpoint, every brand

The Problem: Events for brands you don't handle arrive at your endpoint.

Why It Happens: Endpoints are account-wide. There is no documented per-brand or per-event filter.

Workarounds:

  • Route or drop events by brand_id as the first step after verification.
  • Return 200 for brands you ignore, so they never look like failures.

How Hookdeck Can Help: Hookdeck filters can match on brand_id and type, sending each brand's events to its own destination.

Best practices

Keep the timestamp as text

Use the t value exactly as it appears in the header when you build the signed message. Parsing it to a number and formatting it again can change it.

Verify the raw body

Build the message from the bytes BigMailer sent. Use express.raw() in Express, await request.arrayBuffer() in Next.js and await request.body() in FastAPI. See our guide to webhook idempotency for storing the composite key described above.

Act on hard bounces and complaints first

These two events protect your sending reputation on Amazon SES. Process them before engagement events, and don't let a backlog of opens and clicks delay them. Doing the work after you respond helps, as covered in why to process webhooks asynchronously.

Conclusion

BigMailer sends five engagement events for every brand in an account, signed with a timestamped HMAC-SHA256 in X-BigMailer-Signature. Parse the header fully, verify against the raw body, route by brand_id, and build your own idempotency key, because BigMailer documents neither event IDs nor retries.

Hookdeck Event Gateway verifies each BigMailer signature and stores every event, so a failed request can be retried or replayed instead of lost.

Get started with Hookdeck for free and handle BigMailer webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.