# Mailjet Webhooks

## When to Use This Skill

* How do I receive Mailjet webhooks (Event API / Event tracking)?
* How do I verify or secure a Mailjet webhook? (there is no signature)
* Why is my Mailjet webhook endpoint receiving an `Authorization: Basic` header?
* How do I handle `sent`, `open`, `click`, `bounce`, `blocked`, `spam`, and `unsub` events?
* Why does my Mailjet webhook receive an array of events instead of one object?
* Why is Mailjet retrying my webhook?

## How Mailjet Webhooks Are Secured

**Mailjet does not sign webhooks and cannot send a custom header.** The only built-in protection is HTTP Basic auth: put credentials in the webhook URL you register, and Mailjet sends them as an `Authorization: Basic base64(username:password)` header.

```
https://username:password@your-domain.com/webhooks/mailjet

```

Always use HTTPS, and use a long random password. Your handler must decode the header and compare the username and password in constant time.

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides so unequal lengths don't throw or leak length
const safeEqual = (a, b) => crypto.timingSafeEqual(
  crypto.createHash('sha256').update(a).digest(),
  crypto.createHash('sha256').update(b).digest());

function verifyMailjet(authHeader, username, password) {
  if (!authHeader || !/^Basic /i.test(authHeader)) return false;
  const decoded = Buffer.from(authHeader.slice(6).trim(), 'base64').toString('utf8');
  const i = decoded.indexOf(':');               // split on FIRST colon only
  if (i < 0) return false;
  const userOk = safeEqual(decoded.slice(0, i), username);
  const passOk = safeEqual(decoded.slice(i + 1), password);
  return userOk && passOk;                      // no short-circuit between the two
}
// Reject with 401 + WWW-Authenticate: Basic when this returns false

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailjet-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailjet-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailjet-webhooks/examples/fastapi/).

## Payload Shape

The body is JSON. By default it is **one event object per request**. If you enable **Group Events** (Event tracking settings, or `version: 2` on `/eventcallbackurl`), Mailjet posts a **JSON array** of events. Handlers should accept both.

Every event has `event`, `time`, `email`, `mj_campaign_id`, `mj_contact_id`, `customcampaign`, `MessageID`, `CustomID`, and `Payload`.

## Common Event Types

| Event | Triggered When | Key Fields |
| --- | --- | --- |
| `sent` | Destination SMTP server accepted the message | `smtp_reply`, `mj_message_id` |
| `open` | Recipient opened the email | `ip`, `geo`, `agent` |
| `click` | Recipient clicked a tracked link | `url`, `ip`, `geo`, `agent` |
| `bounce` | Delivery failed | `hard_bounce`, `blocked`, `error_related_to`, `error`, `comment` |
| `blocked` | Mailjet or the recipient side blocked the send | `error_related_to`, `error` |
| `spam` | Recipient reported the message as spam | `source` |
| `unsub` | Recipient unsubscribed | `mj_list_id`, `ip`, `geo`, `agent` |

> **For the full event reference**, see [Mailjet event types](https://dev.mailjet.com/docs/email-api/webhooks/event-types).

## Environment Variables

```bash
# Must match the user:pass embedded in the webhook URL registered with Mailjet
MAILJET_WEBHOOK_USERNAME=mailjet
MAILJET_WEBHOOK_PASSWORD=change-me-to-a-long-random-string

```

## Gotchas

* **Respond `200` quickly.** Any other status makes Mailjet retry every 30 seconds for up to 24 hours, and a persistently failing URL can be suspended (reactivate by updating it with a new URL). Process events asynchronously.
* **No event ID or timestamp-based replay protection.** Mailjet retries and `sent` volume make duplicates likely; dedupe on `MessageID` + `event` + `time` (+ `email`).
* **Passwords may contain `:`.** Split the decoded credentials on the first colon only. Percent-encode special characters in the URL when registering it.
* **`sent` is high-volume.** Turn on Group Events so you receive batches.
* `bounce` events with `hard_bounce: false` are temporary; only suppress addresses on hard bounces.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 mailjet --path /webhooks/mailjet

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailjet-webhooks/references/overview.md) — Event catalog and payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailjet-webhooks/references/setup.md) — Registering the webhook URL in the Mailjet dashboard or API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailjet-webhooks/references/verification.md) — Basic auth verification details and gotchas