MisarMisar Docs
MisarMailMisarBlogMisarReachMisarPostMisarDevMisarCoderMisarSEOMisar PlatformMisar SSO
API Reference

Webhooks

Receive real-time event notifications for email opens, clicks, bounces, and more

MisarMail sends outbound webhooks to your endpoint when email events occur. Configure webhooks in Settings → Webhooks.

Configuration

Create a Webhook Endpoint

  1. Go to Settings → Webhooks → New Endpoint
  2. Enter your HTTPS URL
  3. Select event types to subscribe to
  4. Copy the signing secret

Event Types

Outbound webhooks are dispatched for the following events:

EventDescription
contact.createdA new contact was added
contact.updatedA contact's details changed
contact.unsubscribedA contact unsubscribed
campaign.sentA campaign was sent
campaign.completedCampaign processing finished
email.openedRecipient opened the email
email.clickedRecipient clicked a tracked link
email.bouncedHard or soft bounce received

Inbound email delivery uses a separate email.received webhook with a different payload — see Inbound Email Webhooks below.


Webhook Payload

All outbound events share the same envelope: a top-level event, an ISO-8601 timestamp, and an event-specific data object.

{
  "event": "email.opened",
  "timestamp": "2026-02-17T12:00:00.000Z",
  "data": {
    "message_id": "msg_abc123",
    "email": "user@example.com",
    "campaign_id": "550e8400-e29b-41d4-a716-446655440001"
  }
}

Bounce Event

{
  "event": "email.bounced",
  "timestamp": "2026-02-17T12:00:00.000Z",
  "data": {
    "message_id": "msg_abc123",
    "email": "user@example.com",
    "bounce_type": "hard"
  }
}

Request headers

Every outbound webhook request includes:

HeaderValue
X-MisarMail-EventThe event name, e.g. email.opened
X-MisarMail-TimestampISO-8601 dispatch time
X-MisarMail-Signaturesha256=<hex> HMAC of the raw body (only sent when a signing secret ≥ 16 chars is configured)
User-AgentMisarMail-Webhooks/1.0

Signature Verification

When a signing secret is configured, each request includes an X-MisarMail-Signature header formatted as sha256=<hex>. Strip the sha256= prefix, then compute your own HMAC over the raw body and compare.

Verification Algorithm

  1. Read the raw request body as a string
  2. Strip the sha256= prefix from the header value
  3. Compute HMAC-SHA256 (hex) over the raw body using your webhook signing secret
  4. Compare with the header value (constant-time comparison)
import crypto from "crypto";

export function verifyWebhookSignature(
  payload: string,
  header: string, // value of X-MisarMail-Signature, e.g. "sha256=abc123..."
  secret: string
): boolean {
  const signature = header.replace(/^sha256=/, "");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload, "utf8")
    .digest("hex");

  const sigBuffer = Buffer.from(signature, "hex");
  const expBuffer = Buffer.from(expected, "hex");

  if (sigBuffer.length !== expBuffer.length) return false;

  return crypto.timingSafeEqual(sigBuffer, expBuffer);
}
// Next.js App Router route handler
export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get("x-misarmail-signature") ?? "";
  const secret = process.env.MISARMAIL_WEBHOOK_SECRET!;

  if (!verifyWebhookSignature(body, sig, secret)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const payload = JSON.parse(body);

  switch (payload.event) {
    case "email.bounced":
      await handleBounce(payload.data);
      break;
    case "contact.unsubscribed":
      await handleUnsubscribe(payload.data);
      break;
    case "campaign.completed":
      await handleCampaignCompleted(payload.data);
      break;
  }

  return new Response("OK");
}

Inbound Email Webhooks

When you configure an inbound domain (via POST /v1/inbound), MisarMail delivers incoming emails to your webhook endpoint as email.received events. Unlike outbound webhooks, the inbound payload is flat — fields sit at the top level rather than inside a data envelope.

Inbound Payload

{
  "event": "email.received",
  "id": "msg_inb_abc123",
  "from": "sender@example.com",
  "to": "support@yourdomain.com",
  "cc": "team@yourdomain.com",
  "subject": "Help request",
  "text": "Hi, I need help with my account.",
  "html": "<p>Hi, I need help with my account.</p>",
  "headers": {
    "message-id": "<abc123@example.com>",
    "reply-to": "sender@example.com"
  },
  "attachments": [
    {
      "filename": "screenshot.png",
      "contentType": "image/png",
      "size": 48210,
      "contentId": "<part1.abc@example.com>"
    }
  ],
  "spam_score": 0.2,
  "message_id": "<abc123@example.com>",
  "received_at": "2026-02-17T12:00:00.000Z"
}

from and to are plain address strings. cc and attachment contentId are optional. Attachment metadata is inline (filename, contentType, size, optional contentId) — there is no separate download URL.

Inbound Signature Verification

Inbound email webhooks use the same sha256=<hex> HMAC-SHA256 signature scheme. The X-MisarMail-Signature header is present on all inbound webhook requests. Verify it with the same verifyWebhookSignature function shown above (which strips the sha256= prefix) using your inbound webhook secret. Failed deliveries are retried up to 3 times with short backoff.

Inbound webhooks require an active inbound domain configuration. See the Inbound Domains section for setup instructions.


Delivery & Failure Handling

Outbound webhooks are delivered with a single attempt — there is no automatic retry or backoff schedule. Delivery behavior:

  • Each event is POSTed once, with a 10-second timeout.
  • Every attempt (success or failure) is recorded in the webhook_deliveries log.
  • A non-2xx response or timeout increments the endpoint's consecutive failure count.
  • After 10 consecutive failures, the endpoint is automatically disabled and stops receiving events until you re-enable it in Settings → Webhooks.

Inbound email.received webhooks are handled separately and are retried (up to 3 attempts with short backoff). The single-attempt policy above applies to outbound event webhooks.

Best Practices

  • Respond with 200 quickly, then process asynchronously.
  • Keep your endpoint healthy — repeated failures auto-disable it after 10 in a row.
  • Use the X-MisarMail-Event header to route events without parsing the body first.

Testing Webhooks

Use the Test button in Settings → Webhooks to send a test payload to your endpoint, or use a service like webhook.site during development.


Provider Webhooks (Inbound)

MisarMail receives delivery events from email providers and ISPs via these inbound webhook endpoints. These are for ISP/provider integrations, not for your app to consume — they're documented here for transparency.

Feedback Loop (FBL)

POST/mail/webhooks/fbl

Feedback Loop (FBL) webhook — receives ARF (RFC 5965) complaint reports from ISPs (Yahoo/AOL, Microsoft JMRP, etc.).

When suppressed: true, the recipient is added to the suppression list and their contact status is set to complained. Duplicate FBL reports (same message ID) are idempotently ignored. A GET /api/webhooks/fbl health check returns { status: "ok", accepts: ["message/rfc822", "application/json", "text/plain"] }.

Auth: X-Webhook-Secret: FBL_WEBHOOK_SECRET header (or Authorization: Bearer FBL_WEBHOOK_SECRET).

Content-Types accepted:

  • message/rfc822 — raw ARF email
  • application/json — JSON wrapper { raw?, email?, message?, body? }
  • text/plain — raw ARF text

Response fields

successboolean

true when the report was processed.

feedbackTypestring

The ARF feedback type, e.g. abuse.

recipientstring

The complaining recipient's email address.

suppressedboolean

true when the recipient was added to the suppression list.

200 — OK
{
  "success": true,
  "feedbackType": "abuse",
  "recipient": "complained-user@example.com",
  "suppressed": true
}

Mailcow delivery status

POST/mail/webhooks/mailcow

Mailcow delivery status webhook. Receives bounce, delivery, and complaint events from the Mailcow SMTP server. Events are normalized and written to the email_events and bounce_events tables.

Auth: x-inbound-secret header matching MAILCOW_WEBHOOK_SECRET env var.

Request body

payloadobjectbodyrequired

Mailcow-formatted event JSON.

Request
POST /api/webhooks/mailcow
x-inbound-secret: MAILCOW_WEBHOOK_SECRET

Postal mail server

POST/mail/webhooks/postal

Postal mail server webhook. Receives delivery status and bounce events from the Postal SMTP platform. Events handled: MessageSent, MessageDelivered, MessageDelayed, MessageHeld, MessageBounced, MessageDeliveryFailed, MessageLinkClicked, DomainDNSError.

Auth: IP allowlist + X-Postal-Signature header validation.

Request
POST /api/webhooks/postal
X-Postal-Signature: <signature>

Auto-translate

POST/mail/webhooks/auto-translate

Auto-translation webhook — processes incoming emails and auto-translates them for multilingual inbox support. Translates and updates the emails table.

Auth: Internal only (x-internal-secret header).

Request body

emailIdstringbodyrequired

ID of the email to translate.

targetLanguagestringbodyrequired

Language to translate the email into.

textstringbody

Plain-text email body to translate.

htmlstringbody

HTML email body to translate.

Request
POST /api/webhooks/auto-translate
x-internal-secret: <secret>