# 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`, `spam` or `click` events?
* 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).

```javascript
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_body, hashlib.sha256).hexdigest()`, then
`hmac.compare_digest(received.lower().encode(), expected.encode())` (bytes on both
sides, so a non-ASCII header gives 401, not 500).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailtrap-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailtrap-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailtrap-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailtrap-webhooks/references/verification.md).

## Payload Shape (batched)

Mailtrap batches up to **500 events** per request, flushed every 30 seconds. You
choose the format per webhook:

```json
{ "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 an `events` array.
* **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_log.user.updated`, with `description`,
`actor`, optional `resource` and `changes`, and no `event_id`.

Full list and fields: [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailtrap-webhooks/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/24` and `5.181.200.0/24`.
* Other auth options (Basic auth in the URL, a token query parameter) exist; this skill covers the signature.

## Environment Variables

```bash
# 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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailtrap-webhooks/references/overview.md) - What Mailtrap webhooks are, events, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailtrap-webhooks/references/setup.md) - Dashboard configuration, signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailtrap-webhooks/references/verification.md) - Signature verification details, gotchas, debugging