Guide to Mailtrap Webhooks: Features and Best Practices
Mailtrap webhooks report what happens to the email you send through Mailtrap's Email API and SMTP service: deliveries, bounces, opens, clicks, unsubscribes and spam complaints. On Enterprise plans they also report Audit Log activity in the account. If you send email through Mailtrap, webhooks are how those outcomes reach your own systems, such as suppression lists, CRM records and analytics, almost as they happen.
This guide covers how Mailtrap webhooks are configured and signed, the batched payload formats, the events you can receive, the retry and pause behaviour, and the practices that keep a Mailtrap receiver reliable.
What are Mailtrap webhooks?
A Mailtrap webhook is a URL configured in the Mailtrap app for one area of the product, such as Email Sending or the Audit Log. Mailtrap collects events and sends them in batches: up to 500 events per POST, flushed every 30 seconds when there is anything to send. You choose per webhook whether a batch arrives as a JSON object with an events array or as JSON Lines, one event per line.
Every request carries a Mailtrap-Signature header with the hex HMAC-SHA256 of the raw body. The key is the webhook's signing secret, a string of 32 hex characters. Mailtrap's samples use that string as text, so don't hex-decode it.
Mailtrap webhook features
| Feature | Details |
|---|---|
| Configuration | The Mailtrap app: Webhooks > Create New Webhook |
| Areas | Audit Log, Email Sending, Inbound Inboxes, Campaigns, Reputation Monitor |
| Email Sending filters | Stream (transactional or bulk), domain, and event types |
| Payload format | JSON ({"events": [...]}, application/json) or JSON Lines (application/jsonl) |
| Batching | Up to 500 events per request, sent every 30 seconds |
| Signature header | Mailtrap-Signature: <hex> |
| Scheme | HMAC-SHA256, hex digest, no prefix |
| Signed content | The raw request body, in either format |
| Secret | 32 hex characters per webhook, used as a UTF-8 string |
| Secret reset | The old secret stops working immediately |
| Idempotency key | event_id on each sending event |
| Acknowledgement | 200 within 30 seconds |
| Retries | 40 retries, every 5 minutes. Then the webhook is paused |
| Ports | 443 (HTTPS) or 80 (HTTP) only |
| Source IPs | 45.158.83.0/24 and 5.181.200.0/24. Webhooks only: 45.158.83.129, 45.158.83.130 |
Common events
Email Sending webhooks carry nine event types in the event field:
| Event | Meaning | Extra fields |
|---|---|---|
delivery | Email successfully delivered | |
bounce | Permanent delivery failure | response, response_code, bounce_category |
soft bounce | Temporary failure that Mailtrap will retry | response, response_code, bounce_category |
spam | Recipient reported spam | |
unsubscribe | Recipient unsubscribed | ip, user_agent |
open | Email was opened | ip, user_agent |
click | Link was clicked | ip, user_agent, url |
suspension | Message suspended | reason |
reject | Message rejected | reason |
Note the space in soft bounce. Every sending event also has timestamp (Unix seconds), sending_stream, message_id, email, event_id and sending_domain_name, plus category and custom_variables when you set them at send time. Here is Mailtrap's delivery example in JSON format:
{
"events": [
{
"event": "delivery",
"timestamp": 1728669700,
"sending_stream": "transactional",
"category": "Password reset",
"custom_variables": {
"user_id": "123"
},
"message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6",
"email": "receiver@example.com",
"event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc",
"sending_domain_name": "examplesender.com"
}
]
}
A batch can mix event types, so read event on each element, not just the first. In JSON Lines format the same objects arrive one per line with no events wrapper.
Audit Log events, available on Enterprise, have a different shape: event (for example activity_log.user.login or activity_), description, actor, and optional resource and changes, plus timestamp. Mailtrap lists the Audit Log categories as user logins, profile updates, permission changes "and more", and documents no event names or payloads for the Inbound Inboxes, Campaigns and Reputation Monitor areas.
Setting up Mailtrap webhooks
- In Mailtrap, go to Webhooks and click Create New Webhook.
- Enter the webhook URL. It must use port 443 or 80.
- Choose the payload format: JSON or JSON Lines.
- Choose the area. For Email Sending, also choose the stream, the domain and the event types.
- Click Run Test. Mailtrap sends a dummy payload, and only a
200response counts as a pass. - Save, then open the webhook's details and copy the signing secret, for example into
MAILTRAP_WEBHOOK_SECRET.
Mailtrap also offers basic auth credentials in the URL, or a token as a query parameter, as alternatives or additions to the signature. The signature is the stronger of these, because it covers the body.
If you receive Mailtrap webhooks through Hookdeck, create a Mailtrap source and paste the 32-character secret exactly as shown. Hookdeck uses it as text, the same way Mailtrap does.
Securing Mailtrap webhooks
Read Mailtrap-Signature, compute an HMAC-SHA256 of the raw body with the secret as a UTF-8 key, hex-encode it, and compare in constant time. The process is the same for JSON and JSON Lines bodies.
const crypto = require('crypto');
const express = require('express');
function verifyMailtrapSignature(rawBody, header, secret) {
if (!header || !secret) return false;
const received_hex = header.trim();
if (!/^[0-9a-f]{64}$/i.test(received_hex)) return false;
// The 32-hex-character secret is used as text, not hex-decoded.
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
'utf8'
);
const received = Buffer.from(received_hex.toLowerCase(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
// '*/*' keeps both application/json and application/jsonl bodies raw.
app.post('/webhooks/mailtrap', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.MAILTRAP_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyMailtrapSignature(req.body, req.get('mailtrap-signature'), secret)) {
return res.sendStatus(401);
}
const text = req.body.toString('utf8');
const events = (req.get('content-type') || '').includes('jsonl')
? text.split('\n').filter((line) => line.trim()).map((line) => JSON.parse(line))
: JSON.parse(text).events;
// Deduplicate each event on event_id, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import re
def verify_mailtrap_signature(raw_body: bytes, header, secret: str) -> bool:
if not header or not secret:
return False
received = header.strip()
if not re.fullmatch(r"[0-9a-fA-F]{64}", received):
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received.lower().encode("utf-8"), expected.encode("utf-8"))
express.raw({ type: 'application/json' }) would leave a JSON Lines body unread, so mount */* on the route. Mailtrap recommends answering a request with no signature header with 400. Either 400 or 401 rejects it, and Mailtrap's own deliveries always carry the header.
Make Mailtrap webhooks production-ready. Hookdeck Event Gateway verifies the
Mailtrap-Signatureheader, stores every batch of delivery, bounce and engagement events, and delivers them to your services with retries you control, so a long outage never pauses your Mailtrap webhook.
Mailtrap webhook limitations and pain points
A failed webhook is paused
The Problem: After an outage, events stop arriving even though your endpoint is healthy again.
Why It Happens: Mailtrap retries a failed batch 40 times, every 5 minutes, which covers a bit over three hours. Anything other than 200, or no response within 30 seconds, counts as a failure. If every retry fails, Mailtrap pauses the webhook and emails you, and it stays paused until someone resumes it in the settings.
Workarounds:
- Return
200straight after verification and do the work afterwards. - Return exactly
200. Mailtrap documents200as the acknowledgement and treats any other status as a reason to retry. - Watch for Mailtrap's pause email and resume the webhook as soon as your endpoint recovers.
How Hookdeck Can Help: Hookdeck acknowledges each batch with a 200 as soon as it arrives and stores it, then delivers it to your service with automatic retries on your own schedule. Your downtime then delays events instead of pausing the Mailtrap webhook.
A hex-looking secret used as text
The Problem: Verification fails even with the right secret.
Why It Happens: The signing secret is 32 hex characters, which invites decoding it to 16 bytes, as some vendors require. Mailtrap's samples use the string itself as the key, so a decoded key produces a different HMAC.
Workarounds:
- Pass the secret to the HMAC as a string, or its UTF-8 bytes.
- Test against a real delivery or a Run Test request before going live.
How Hookdeck Can Help: Hookdeck's Mailtrap source type uses the secret as text and checks the bare hex header for you. Requests that fail are marked as failed verification and are not delivered to your destinations.
Batches and two formats
The Problem: Events go missing because the handler only reads one event per request, or parsing fails when the format changes.
Why It Happens: One request can hold up to 500 events of mixed types. JSON bodies wrap them in events, while JSON Lines bodies are newline-separated objects with no wrapper.
Workarounds:
- Loop over every event, and decide the parser from
Content-Typeor from the format you configured. - Process events individually after the request is acknowledged, so one bad event can't fail the whole batch.
How Hookdeck Can Help: Hookdeck transformations can reshape a batch before it reaches your service, and filters can route requests by their contents.
Resetting the secret has no overlap
The Problem: Resetting the signing secret causes failed verifications until every receiver is updated.
Why It Happens: The old secret becomes invalid the moment you reset it.
Workarounds:
- Reset during a quiet period and deploy the new secret immediately. Failed batches will be retried every 5 minutes.
How Hookdeck Can Help: With Hookdeck in front, the secret lives on one source. Update it there and every service behind it keeps receiving verified events.
Best practices
Verify against the raw body
The signature covers the exact bytes Mailtrap sent, in either format. Use express.raw({ type: '*/*' }) in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse only after the check passes.
Deduplicate on event_id
Retries resend whole batches, so the same event can arrive more than once. Store each event_id and skip repeats. See our guide to webhook idempotency.
Handle hard and soft bounces differently
A bounce is permanent, so add the address to your suppression list. A soft bounce is temporary and Mailtrap will retry it, so record it without suppressing the address straight away.
Allowlist Mailtrap's IP ranges
Restrict the endpoint to 45.158.83.0/24 and 5.181.200.0/24, or to 45.158.83.129 and 45.158.83.130 for webhooks only, as well as checking the signature. Mailtrap publishes the ranges at https://mailtrap.io/ip-ranges.json.
Acknowledge fast and process asynchronously
Mailtrap waits 30 seconds for a 200. A batch of 500 events can take longer than that to process, so store it, return 200, and work through it afterwards. See why to process webhooks asynchronously.
Conclusion
Mailtrap signs every batch with Mailtrap-Signature, a bare hex HMAC-SHA256 of the raw body keyed with the 32-character secret used as text. Verify against the raw bytes in either format, loop over every event in the batch, deduplicate on event_id, and answer with 200 within 30 seconds, because a webhook that keeps failing is paused until someone resumes it.
Hookdeck Event Gateway sits in front of your receiver, checks every Mailtrap signature, and holds each batch until your service has processed it.
Get started with Hookdeck for free and handle Mailtrap webhooks reliably in minutes.