Gareth Wilson Gareth Wilson

Guide to Mailtrap Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationThe Mailtrap app: Webhooks > Create New Webhook
AreasAudit Log, Email Sending, Inbound Inboxes, Campaigns, Reputation Monitor
Email Sending filtersStream (transactional or bulk), domain, and event types
Payload formatJSON ({"events": [...]}, application/json) or JSON Lines (application/jsonl)
BatchingUp to 500 events per request, sent every 30 seconds
Signature headerMailtrap-Signature: <hex>
SchemeHMAC-SHA256, hex digest, no prefix
Signed contentThe raw request body, in either format
Secret32 hex characters per webhook, used as a UTF-8 string
Secret resetThe old secret stops working immediately
Idempotency keyevent_id on each sending event
Acknowledgement200 within 30 seconds
Retries40 retries, every 5 minutes. Then the webhook is paused
Ports443 (HTTPS) or 80 (HTTP) only
Source IPs45.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:

EventMeaningExtra fields
deliveryEmail successfully delivered
bouncePermanent delivery failureresponse, response_code, bounce_category
soft bounceTemporary failure that Mailtrap will retryresponse, response_code, bounce_category
spamRecipient reported spam
unsubscribeRecipient unsubscribedip, user_agent
openEmail was openedip, user_agent
clickLink was clickedip, user_agent, url
suspensionMessage suspendedreason
rejectMessage rejectedreason

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_log.user.updated), 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

  1. In Mailtrap, go to Webhooks and click Create New Webhook.
  2. Enter the webhook URL. It must use port 443 or 80.
  3. Choose the payload format: JSON or JSON Lines.
  4. Choose the area. For Email Sending, also choose the stream, the domain and the event types.
  5. Click Run Test. Mailtrap sends a dummy payload, and only a 200 response counts as a pass.
  6. 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-Signature header, 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 200 straight after verification and do the work afterwards.
  • Return exactly 200. Mailtrap documents 200 as 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-Type or 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.