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
- Go to Settings → Webhooks → New Endpoint
- Enter your HTTPS URL
- Select event types to subscribe to
- Copy the signing secret
Event Types
Outbound webhooks are dispatched for the following events:
| Event | Description |
|---|---|
contact.created | A new contact was added |
contact.updated | A contact's details changed |
contact.unsubscribed | A contact unsubscribed |
campaign.sent | A campaign was sent |
campaign.completed | Campaign processing finished |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a tracked link |
email.bounced | Hard 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:
| Header | Value |
|---|---|
X-MisarMail-Event | The event name, e.g. email.opened |
X-MisarMail-Timestamp | ISO-8601 dispatch time |
X-MisarMail-Signature | sha256=<hex> HMAC of the raw body (only sent when a signing secret ≥ 16 chars is configured) |
User-Agent | MisarMail-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
- Read the raw request body as a string
- Strip the
sha256=prefix from the header value - Compute HMAC-SHA256 (hex) over the raw body using your webhook signing secret
- 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_deliverieslog. - 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
200quickly, then process asynchronously. - Keep your endpoint healthy — repeated failures auto-disable it after 10 in a row.
- Use the
X-MisarMail-Eventheader 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)
/mail/webhooks/fblFeedback 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 emailapplication/json— JSON wrapper{ raw?, email?, message?, body? }text/plain— raw ARF text
Response fields
successbooleantrue when the report was processed.
feedbackTypestringThe ARF feedback type, e.g. abuse.
recipientstringThe complaining recipient's email address.
suppressedbooleantrue when the recipient was added to the suppression list.
{
"success": true,
"feedbackType": "abuse",
"recipient": "complained-user@example.com",
"suppressed": true
}Mailcow delivery status
/mail/webhooks/mailcowMailcow 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
payloadobjectbodyrequiredMailcow-formatted event JSON.
POST /api/webhooks/mailcow
x-inbound-secret: MAILCOW_WEBHOOK_SECRETPostal mail server
/mail/webhooks/postalPostal 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.
POST /api/webhooks/postal
X-Postal-Signature: <signature>Auto-translate
/mail/webhooks/auto-translateAuto-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
emailIdstringbodyrequiredID of the email to translate.
targetLanguagestringbodyrequiredLanguage to translate the email into.
textstringbodyPlain-text email body to translate.
htmlstringbodyHTML email body to translate.
POST /api/webhooks/auto-translate
x-internal-secret: <secret>