Webhooks
Receive real-time event notifications for email opens, clicks, bounces, and more
Webhooks
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": "user@example.com",
"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": "user@example.com",
"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": "sender@example.com",
"to": "support@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",
"content_type": "image/png",
"size": 48210,
"url": "https://api.mail.misar.io/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.