Agent skill
Mailtrap Webhooks Skill
Receive and verify Mailtrap webhooks. Use when setting up Mailtrap Email API/SMTP webhook handlers, debugging Mailtrap-Signature verification (HMAC-SHA256 hex over the raw body), handling batched JSON or JSON Lines payloads, or handling sending events like delivery, bounce, soft bounce, open, click, spam and unsubscribe. Also covers Audit Log activity_log.* events.
Install this skill
npx skills add hookdeck/webhook-skills --skill mailtrap-webhooks
When to Use This Skill
- How do I receive Mailtrap webhooks?
- How do I verify a Mailtrap webhook signature (
Mailtrap-Signature)? - Why is my Mailtrap signature verification failing?
- How do I handle Mailtrap
delivery,bounce,soft bounce,spamorclickevents? - How do I parse Mailtrap's batched JSON / JSON Lines webhook payloads?
- How do I handle Mailtrap Audit Log (
activity_log.*) webhooks?
Verification (core)
Mailtrap-Signature: <lowercase hex HMAC-SHA256 of the RAW request body>, keyed with the webhook's signing secret (32 hex characters) used as a plain UTF-8 string, not hex-decoded. No timestamp, no prefix. The body is signed regardless of Content-Type (JSON or JSON Lines).
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
if (!signature || !secret) return false;
const received = String(signature).trim();
// Buffer.from(x, 'hex') silently truncates invalid hex: validate first
if (!/^[0-9a-f]{64}$/i.test(received)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(received.toLowerCase(), 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: read the raw body for ANY content type (json or jsonl)
app.post('/webhooks/mailtrap', express.raw({ type: '*/*' }), (req, res) => {
if (!verifySignature(req.body, req.header('Mailtrap-Signature'), process.env.MAILTRAP_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// ...parse, then return 200
});
Python equivalent: hmac.new(secret.encode(), raw_, then hmac.compare_ (bytes on both sides, so a non-ASCII header gives 401, not 500).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Mailtrap's own Node sample uses Buffer.from(x, 'hex') and express.raw({ type: 'application/json' }). Don't copy those: the first accepts truncated garbage and the second leaves JSON Lines bodies unparsed. See references/verification.md.
Payload Shape (batched)
Mailtrap batches up to 500 events per request, flushed every 30 seconds. You choose the format per webhook:
{ "events": [ { "event": "delivery", "email": "receiver@example.com", "event_id": "bede7236-...", "message_id": "1df37d17-...", "timestamp": 1728669700, "sending_stream": "transactional", "sending_domain_name": "examplesender.com" } ] }
- JSON (
application/json): an object with aneventsarray. - JSON Lines (
application/jsonl): one event object per line, no wrapper.
Always loop over every event. event_id is the documented idempotency key.
Event Types
Sending events use the event field (note soft bounce contains a space):
| Event | Fires when |
|---|---|
delivery | Email successfully delivered |
bounce | Permanent delivery failure (response, response_code, bounce_category) |
soft bounce | Temporary failure, Mailtrap will retry |
spam | Recipient reported spam |
unsubscribe | Recipient unsubscribed |
open | Email opened (ip, user_agent) |
click | Link clicked (url, ip, user_agent) |
suspension | Message suspended (reason) |
reject | Message rejected (reason) |
Audit Log webhooks (Enterprise only) send activity_log.* types such as activity_log.user.login and activity_, with description, actor, optional resource and changes, and no event_id.
Full list and fields: references/overview.md.
Delivery Semantics
- Return 200 to acknowledge. Any other status is retried: 40 retries, every 5 minutes. If all fail, the webhook is paused and you get an email; resume it manually.
- Timeout is 30 seconds. Acknowledge first, process asynchronously.
- No timestamp or nonce is signed, so replay protection is application-level: dedupe on
event_id. - Webhooks are delivered only to ports 443 (HTTPS) or 80 (HTTP), from
45.158.83.0/24and5.181.200.0/24. - Other auth options (Basic auth in the URL, a token query parameter) exist; this skill covers the signature.
Environment Variables
# The webhook's signing secret (32 hex chars) from its details in Mailtrap.
# Use it as plain text. This is NOT your Mailtrap API token.
MAILTRAP_WEBHOOK_SECRET=your_32_hex_character_signing_secret
PORT=3000
Local Development
# No install, no account required — creates a guest account on first run
npx hookdeck-cli listen 3000 mailtrap --path /webhooks/mailtrap
Paste the printed URL into the webhook URL field, then click Run Test in Mailtrap. Use 8000 instead of 3000 for the FastAPI example.
Reference Materials
- references/overview.md - What Mailtrap webhooks are, events, payload fields
- references/setup.md - Dashboard configuration, signing secret
- references/verification.md - Signature verification details, gotchas, debugging