Agent skill

BigMailer Webhooks Skill

Receive and verify BigMailer webhooks. Use when setting up BigMailer webhook handlers, debugging X-BigMailer-Signature verification, or handling email engagement events like bounce, click, complaint, open, and unsubscribe.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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)

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/, examples/nextjs/, examples/fastapi/.

Common Event Types

EventDescription
bounceA message bounced; bounce.type is hard or soft
clickA recipient clicked a link (click.url, click.link_id)
complaintA recipient reported the message as spam
openA recipient opened a message
unsubscribeA 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.

For the full event reference, see BigMailer's webhook documentation.

Important Headers

HeaderDescription
X-BigMailer-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>]

Environment Variables

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

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →