> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

# Webhooks

Mailofly can POST signed JSON to your HTTPS endpoint when mail log status changes — no polling required.

Configure endpoints in the panel under **API → Webhooks** (organization admins).

***

## Events

| Event | When it fires |
| - | - |
| `mail.sent` | Message handed off to the provider |
| `mail.failed` | Send attempt failed |
| `mail.deferred` | Queued for a later day (daily quota) |
| `mail.halted` | Send halted (when that status is written) |

Open/click and provider delivery/bounce events (`mail.delivered`, `mail.bounced`) are not available yet.

***

## Source filter

Each webhook can subscribe to a **source**:

| Source | Matches |
| - | - |
| `all` | Any mail log |
| `transactional` | `campaign_id` is null |
| `campaign` | `campaign_id` is set |

The payload always includes `campaign_id` (nullable) so consumers can branch further.

***

## Payload shape

```json theme={null}
{
  "type": "mail.sent",
  "created_at": "2026-08-08T12:00:00.000Z",
  "data": {
    "mail_log_id": "uuid",
    "to_email": "user@example.com",
    "subject": "Hello",
    "status": "sent",
    "campaign_id": null,
    "campaign_run_id": null,
    "account_id": "uuid",
    "contact_id": null,
    "error": null,
    "sent_at": "2026-08-08T12:00:00.000Z",
    "organization_id": "uuid"
  }
}
```

***

## Request headers

| Header | Description |
| - | - |
| `Content-Type` | `application/json` |
| `X-Mailofly-Event` | Event name (e.g. `mail.sent`) |
| `X-Mailofly-Delivery-Id` | Unique delivery id (use for idempotency) |
| `X-Mailofly-Timestamp` | Unix seconds when the request was signed |
| `X-Mailofly-Signature` | `t=<timestamp>,v1=<hex>` HMAC-SHA256 |

Signing string: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")` where `rawBody` is the exact JSON body bytes.

***

## Verify signature (Node.js)

```js theme={null}
import { createHmac, timingSafeEqual } from "crypto";

function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const [k, ...rest] = p.trim().split("=");
      return [k, rest.join("=")];
    }),
  );
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > toleranceSec) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

Respond with **2xx** quickly. Non-2xx responses are retried with backoff (about 1m → 5m → 30m → 2h, up to 5 attempts).

***

## Delivery worker (ops)

Retries and backlog are drained by:

`POST /webhooks/dispatch`

Auth: header `x-campaign-automation-secret` must match `CAMPAIGN_AUTOMATION_SECRET` (same secret as the mail queue worker).

Optional JSON body: `{ "limit": 20 }` (1–100).

Schedule with pg\_cron → pg\_net every minute (same pattern as `/api/mail/process-mail-queue`). New deliveries are also kicked best-effort in-process right after enqueue.

***

## Related

* [Mail logs](https://docs.mailofly.com/guides/mail-logs)
* [API: Mail logs](https://docs.mailofly.com/api/mail-logs)
* [REST API automation](/docs/integrations/rest-api-automation.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.