Webhooks

Mailfy POSTs a signed JSON event to your endpoint whenever an email changes state. Create endpoints under Webhooks in the dashboard and choose the events to receive.

Events

TypeWhen
email.sentHanded to the delivery provider.
email.deliveredThe recipient’s server accepted it.
email.delivery_delayedTemporary problem; the provider keeps trying.
email.bouncedPermanent bounce. The address is suppressed.
email.complainedMarked as spam. The address is suppressed.
email.opened, email.clickedEngagement, when open and click tracking is enabled on the sending setup.
email.failedCould not be sent; the reason is in data.error.

Payload

JSON
{
  "id": "evt_…",
  "type": "email.delivered",
  "createdAt": "2026-11-13T09:24:11.512Z",
  "data": {
    "emailId": "6f1c…",
    "projectId": "…",
    "from": "Acme <orders@acme.com>",
    "to": ["ada@example.com"],
    "subject": "Your receipt",
    "tags": { "order": "1042" }
  }
}

Bodies and attachments are never included. Respond with any 2xx within 10 seconds. Failed deliveries are retried with exponential backoff about 8 times over roughly two hours; each attempt is visible in the dashboard and can be retried by hand.

Verifying signatures

Every request carries webhook-id, webhook-timestamp and webhook-signature headers (the Standard Webhooks format). The signature is v1, followed by base64 HMAC-SHA256 of {id}.{timestamp}.{raw body}, keyed with your endpoint secret (the base64 part after whsec_). Reject requests older than five minutes.

Node.js
import express from 'express';
import { verifyWebhook } from '@mailfy/node';

app.post('/webhooks/mailfy', express.text({ type: '*/*' }), (req, res) => {
  if (!verifyWebhook(req.body, req.headers, process.env.MAILFY_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // handle event.type …
  res.sendStatus(204);
});
Python
from mailfy import verify_webhook

if not verify_webhook(raw_body, request.headers, os.environ["MAILFY_WEBHOOK_SECRET"]):
    abort(400)

Use the raw body exactly as received: re-serialized JSON will not match the signature. Use webhook-id to ignore duplicates.