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
| Event | Description |
|---|---|
email.sent | Email successfully sent via SMTP |
email.delivered | Delivery confirmed by recipient server |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a tracked link |
email.bounced | Hard or soft bounce received |
email.complained | Spam complaint (FBL report) |
email.unsubscribed | Recipient unsubscribed |
email.received | Inbound email received on a configured domain |
campaign.sent | All campaign emails have been queued |
campaign.completed | Campaign processing finished |
Webhook Payload
All events share the same envelope format:
{
"id": "evt_550e8400e29b41d4a716446655440000",
"type": "email.opened",
"created_at": "2026-02-17T12:00:00.000Z",
"data": {
"message_id": "msg_abc123",
"email": "[email protected]",
"campaign_id": "550e8400-e29b-41d4-a716-446655440001",
"timestamp": "2026-02-17T12:00:00.000Z"
}
}Bounce Event
{
"type": "email.bounced",
"data": {
"message_id": "msg_abc123",
"email": "[email protected]",
"bounce_type": "hard",
"bounce_code": "550",
"bounce_message": "User unknown"
}
}Signature Verification
Every webhook request includes an X-MisarMail-Signature header. Verify it to ensure the request is from MisarMail.
Verification Algorithm
- Read the raw request body as bytes
- Compute HMAC-SHA256 using your webhook signing secret
- Compare with the header value (constant-time comparison)
import crypto from "crypto";
export function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
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 event = JSON.parse(body);
switch (event.type) {
case "email.bounced":
await handleBounce(event.data);
break;
case "email.unsubscribed":
await handleUnsubscribe(event.data);
break;
case "email.received":
await handleInbound(event.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.
Inbound Payload
{
"id": "evt_inbound_abc123",
"type": "email.received",
"created_at": "2026-02-17T12:00:00.000Z",
"data": {
"from": "[email protected]",
"to": "[email protected]",
"subject": "Help request",
"text": "Hi, I need help with my account.",
"html": "<p>Hi, I need help with my account.</p>",
"headers": {
"message-id": "<[email protected]>",
"reply-to": "[email protected]"
},
"attachments": [
{
"filename": "screenshot.png",
"content_type": "image/png",
"size": 48210,
"url": "https://api.misar.io/mail/v1/inbound/attachments/att_xyz"
}
]
}
}Inbound Signature Verification
Inbound email webhooks use the same HMAC-SHA256 signature scheme. The X-MisarMail-Signature header is present on all inbound webhook requests. Verify it using the same verifyWebhookSignature function shown above with your inbound webhook secret.
Inbound webhooks require an active inbound domain configuration. See the Inbound Domains section for setup instructions.
Retry Policy
If your endpoint returns a non-2xx status or times out (30 seconds), MisarMail retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the event is marked as failed and no further retries occur.
Best Practices
- Respond with
200immediately, process asynchronously - Use idempotency — the same event may be delivered more than once
- Check
event.idto deduplicate
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 and for self-hosted deployments.
Feedback Loop (FBL)
/api/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": "[email protected]",
"suppressed": true
}Mailcow delivery status
/api/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_SECRET
Postal mail server
/api/webhooks/postalPostal mail server webhook. Receives delivery status, bounce, and spam reports from the Postal SMTP platform. Events handled: MessageSent, MessageDelivered, MessageBounced, MessageHeld, SpamReport.
Auth: IP allowlist + X-Postal-Signature header validation.
POST /api/webhooks/postal
X-Postal-Signature: <signature>
Auto-translate
/api/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>