# Guide to BigMailer Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | **Webhooks** settings in the BigMailer console |
| Scope | All brands in the account, identified by `brand_id` |
| Signature | hex HMAC-SHA256 |
| Signature header | `x-bigmailer-signature` |
| Header format | `t=<unix>,v1=<signature>`, possibly with more than one `v1` |
| Signed content | `<t>.<raw body>` |
| Secret | One per endpoint, revealed with **show** |
| Body | JSON |
| Event routing | Body field `type` |
| Retries, timeout, success codes | Not documented |

## Events and payloads

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `bounce` | A message bounces (`bounce.type` is `hard` or `soft`) | Suppress hard-bounced addresses, track deliverability |
| `click` | A recipient clicks a tracked link | Engagement scoring, link analytics |
| `complaint` | A recipient marks a message as spam | Suppress the contact, alert on complaint rates |
| `open` | A recipient opens a message | Engagement tracking |
| `unsubscribe` | A recipient unsubscribes | Sync 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](https://github.com/hookdeck/webhook-samples/blob/main/providers/bigmailer/latest/bounce.json):

```json
{
  "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](/docs/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](https://docs.bigmailer.io/docs/verifying-webhook-signatures).

```javascript
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`:

```javascript
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](/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) verifies each BigMailer signature and stores every event, so a failed request can be retried or replayed instead of lost.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle BigMailer webhooks reliably in minutes.