# BigMailer Webhooks

## When to Use This Skill

* How do I receive BigMailer webhooks?
* How do I verify the `X-BigMailer-Signature` header (`t=...,v1=...`)?
* How do I handle BigMailer `bounce`, `click`, `complaint`, `open`, or `unsubscribe` events?
* Why is my BigMailer webhook signature verification failing?

## Essential Code (USE THIS)

BigMailer signs every event with `X-BigMailer-Signature`:

```
X-BigMailer-Signature: t=1492774578,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8be

```

* `t` is the Unix timestamp (seconds). Use it as the exact string from the header.
* `v1` is HMAC-SHA256 of `"<t>.<raw body>"`, keyed with the endpoint's **signing secret** (UTF-8 bytes, used as-is). `v1` is the only valid scheme; ignore other keys. The docs never name the digest encoding; it is 64 hex characters, so compare as hex (case-insensitively).
* The header can carry more than one `v1=` entry (for example during key rotation). Accept the event if **any** entry matches.
* A header with no `t=` value fails. Missing, empty, malformed, or mismatched signature: respond `401`.
* The event type is the top-level `type` field of the body. The data sits under a key named after the type (`bounce`, `click`, ...). `bounce.type` (`hard`/`soft`) is the bounce kind, not the event.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyBigMailer(rawBody, signatureHeader, secret) {
  if (!secret || !signatureHeader) return false;
  let timestamp;
  const signatures = [];
  for (const item of signatureHeader.split(',')) {
    const i = item.indexOf('=');
    if (i <= 0) continue;
    const key = item.slice(0, i).trim(), value = item.slice(i + 1).trim();
    if (key === 't') timestamp = value;
    else if (key === 'v1') signatures.push(value);
  }
  if (!timestamp || signatures.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex')
  );
  return signatures.some((sig) =>
    /^[0-9a-fA-F]{64}$/.test(sig) && crypto.timingSafeEqual(Buffer.from(sig.toLowerCase()), expected));
}

```

### Verification (Python)

```python
import hashlib, hmac, re

def verify_bigmailer(raw_body: bytes, signature_header, secret: str) -> bool:
    if not secret or not signature_header:
        return False
    timestamp, signatures = None, []
    for item in signature_header.split(","):
        key, _, value = item.strip().partition("=")
        if key == "t":
            timestamp = value.strip()
        elif key == "v1":
            signatures.append(value.strip())
    if not timestamp or not signatures:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest().encode()
    return any(re.fullmatch(r"[0-9a-fA-F]{64}", s) and hmac.compare_digest(s.lower().encode(), expected)
               for s in signatures)

```

Validate the signature's format (exactly 64 hex characters) before comparing, then compare in constant time. Never call `Buffer.from(sig, 'hex')` on an unvalidated value; it silently drops bad characters.

**Timestamp window:** the examples do **not** reject old timestamps by default. Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck's own verification applies no tolerance, so a fixed window would reject legitimate retries. To opt in, set `BIGMAILER_WEBHOOK_TOLERANCE_SECONDS` to a positive number of seconds (unset or `0` = off); `t` is Unix seconds.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bigmailer-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bigmailer-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/bigmailer-webhooks/examples/fastapi/).

## Common Event Types

| Event | Description |
| --- | --- |
| `bounce` | A message bounced; `bounce.type` is `hard` or `soft` |
| `click` | A recipient clicked a link (`click.url`, `click.link_id`) |
| `complaint` | A recipient reported the message as spam |
| `open` | A recipient opened a message |
| `unsubscribe` | A recipient unsubscribed (`unsubscribe_all`, `unsubscribe_ids`) |

These are the five documented event types. Each payload also carries `brand_id`, and the nested object has `date`, `campaign_id`, `contact_id`, and `email`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bigmailer-webhooks/references/overview.md).

> **For the full event reference**, see [BigMailer's webhook documentation](https://docs.bigmailer.io/docs/webhooks-getting-started).

## Important Headers

| Header | Description |
| --- | --- |
| `X-BigMailer-Signature` | `t=<unix seconds>,v1=<hex>[,v1=<hex>]` |

## Environment Variables

```bash
BIGMAILER_WEBHOOK_SECRET=your_endpoint_signing_secret   # "show" in the Webhooks settings
BIGMAILER_WEBHOOK_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 bigmailer --path /webhooks/bigmailer

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bigmailer-webhooks/references/overview.md) - BigMailer webhook concepts, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bigmailer-webhooks/references/setup.md) - Creating an endpoint, finding the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/bigmailer-webhooks/references/verification.md) - Signature verification details and gotchas