# MailerLite Webhooks

Covers the new MailerLite API (`connect.mailerlite.com`), not MailerLite Classic.

## When to Use This Skill

* How do I receive MailerLite webhooks?
* How do I verify the MailerLite `Signature` header?
* Why is my MailerLite webhook signature verification failing?
* How do I handle `subscriber.created`, `subscriber.unsubscribed` or `campaign.sent` events?
* How do I handle MailerLite batched webhooks (the `events` array)?

## Verification (core)

MailerLite sends a `Signature` header containing the **bare hex** HMAC-SHA256 of the **raw JSON body**, keyed with the webhook's `secret` as a UTF-8 string. No prefix, no timestamp. The secret is per webhook and is returned by the webhooks API (for example `"secret": "ybmcD7PQ9R"`).

Node:

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

function verify(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signatureHeader.trim().toLowerCase()), Buffer.from(expected));
  } catch {
    return false; // different lengths
  }
}

```

Python:

```python
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str | None, secret: str | None) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.strip().lower().encode(), expected.encode())

```

In Express use `express.raw({ type: '*/*' })` so the raw bytes are available whatever content type is sent. MailerLite has no official webhook verification SDK, so verification is manual.

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

## Payload Shapes

| Shape | Event name field | Example events |
| --- | --- | --- |
| Flat subscriber or campaign object | `event` | `subscriber.created`, `subscriber.updated`, `subscriber.unsubscribed`, `subscriber.bounced`, `subscriber.spam_reported`, `subscriber.active`, `campaign.sent` |
| Nested object | `type` (with `subscriber`, `group`, `automation`, `campaign`) | `subscriber.added_to_group`, `subscriber.automation_completed`, `subscriber.deleted`, `campaign.open`, `campaign.click` |
| Batched | `{ "events": [...], "total": n }`, each item has `type` | `campaign.open`, `campaign.click` (batching required for these, `subscriber.deleted` too) |

## Common Event Types

| Event | Description |
| --- | --- |
| `subscriber.created` | New subscriber added to the account |
| `subscriber.updated` | Custom fields updated or subscription confirmed |
| `subscriber.unsubscribed` | Subscriber becomes unsubscribed |
| `subscriber.added_to_group` | Subscriber added to a group |
| `subscriber.removed_from_group` | Subscriber removed from a group |
| `subscriber.bounced` | Email address bounces |
| `subscriber.spam_reported` | Subscriber marks a campaign as spam |
| `subscriber.automation_triggered` / `subscriber.automation_completed` | Subscriber starts / finishes an automation |
| `subscriber.deleted` | Subscriber deleted or forgotten (`batchable` must be `true`) |
| `subscriber.active` | Subscriber status set to active |
| `campaign.sent` | Campaign sent |
| `campaign.open` / `campaign.click` | Subscriber opens / clicks a link (`batchable` must be `true`) |

> **For the full event reference**, see [MailerLite Webhooks](https://developers.mailerlite.com/docs/webhooks.html).

## Important Details

| Item | Value |
| --- | --- |
| Signature header | `Signature` |
| Algorithm | HMAC-SHA256, hex, of the raw body |
| Timestamp / event ID | None. Use payload fields (subscriber `id`, `updated_at`) for idempotency |
| Response deadline | 3 seconds, 2xx means delivered |
| Retries | 3 more attempts after 10, 100 and 1000 seconds |

## Environment Variables

```bash
MAILERLITE_WEBHOOK_SECRET=your_webhook_secret   # "secret" returned by the webhooks API

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailerlite-webhooks/references/overview.md) - MailerLite webhook concepts, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailerlite-webhooks/references/setup.md) - Creating webhooks through the API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mailerlite-webhooks/references/verification.md) - Signature verification details and gotchas