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-Signatureheader (t=...,v1=...)? - How do I handle BigMailer
bounce,click,complaint,open, orunsubscribeevents? - 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
tis the Unix timestamp (seconds). Use it as the exact string from the header.v1is HMAC-SHA256 of"<t>.<raw body>", keyed with the endpoint's signing secret (UTF-8 bytes, used as-is).v1is 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: respond401. - The event type is the top-level
typefield 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_ 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
| 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.
For the full event reference, see BigMailer's webhook documentation.
Important Headers
| Header | Description |
|---|---|
X-BigMailer-Signature | t=<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
- references/overview.md - BigMailer webhook concepts, events, payloads
- references/setup.md - Creating an endpoint, finding the signing secret
- references/verification.md - Signature verification details and gotchas