Webhooks
Receive real-time delivery, open, click, bounce, and unsubscribe events via webhooks.
Webhooks
MisarMail POSTs JSON events to your endpoint for every email lifecycle event.
Manage webhooks
List
GET /v1/webhooks
Create
POST /v1/webhooks
{
"url": "https://yourapp.com/hooks/misarmail",
"events": ["email.delivered", "email.opened", "email.clicked", "email.bounced", "email.unsubscribed"],
"secret": "whsec_YOUR_SIGNING_SECRET"
}
Update
PATCH /v1/webhooks/:id
Delete
DELETE /v1/webhooks/:id
List recent events
GET /v1/webhooks/:id/events
Returns the last 100 webhook delivery attempts with status codes and response bodies.
Event types
| Event | Fired when |
|-------|-----------|
| email.sent | Email accepted for delivery |
| email.delivered | SMTP accepted by recipient server |
| email.opened | Tracking pixel loaded |
| email.clicked | Tracked link clicked |
| email.bounced | Hard or soft bounce received |
| email.complained | Spam complaint (FBL) received |
| email.unsubscribed | Recipient unsubscribed |
| campaign.sent | Campaign finished sending |
| campaign.completed | All sends and events settled |
| contact.created | New contact added |
| contact.updated | Contact record changed |
Payload structure
All events share a common envelope:
{
"id": "evt_01ABCDEF",
"type": "email.opened",
"createdAt": "2025-06-15T10:30:00Z",
"data": { ... }
}
email.delivered
{
"type": "email.delivered",
"data": {
"messageId": "msg_01ABCDEF",
"to": "[email protected]",
"subject": "Your order shipped",
"campaignId": null,
"deliveredAt": "2025-06-15T10:29:58Z"
}
}
email.opened
{
"type": "email.opened",
"data": {
"messageId": "msg_01ABCDEF",
"email": "[email protected]",
"openedAt": "2025-06-15T10:35:00Z",
"userAgent": "Mozilla/5.0...",
"ip": "203.0.113.42",
"machineOpen": false
}
}
email.clicked
{
"type": "email.clicked",
"data": {
"messageId": "msg_01ABCDEF",
"email": "[email protected]",
"url": "https://yourapp.com/pricing",
"clickedAt": "2025-06-15T10:36:00Z"
}
}
email.bounced
{
"type": "email.bounced",
"data": {
"messageId": "msg_01ABCDEF",
"email": "[email protected]",
"bounceType": "hard",
"bounceCode": "550",
"bounceMessage": "User unknown",
"bouncedAt": "2025-06-15T10:30:05Z"
}
}
Verifying signatures
Every webhook POST includes an X-MisarMail-Signature header. Verify it to reject forged requests:
import { createHmac } from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${expected}` === signature;
}
// In your handler:
const raw = await req.text();
const sig = req.headers.get('x-misarmail-signature') ?? '';
if (!verifyWebhook(raw, sig, process.env.WEBHOOK_SECRET!)) {
return new Response('Forbidden', { status: 403 });
}
const event = JSON.parse(raw);
Retry policy
Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:
| Attempt | Delay | |---------|-------| | 1st retry | 1 min | | 2nd retry | 5 min | | 3rd retry | 30 min | | 4th retry | 2 h | | 5th retry | 8 h |
After 5 failures the event is marked failed and no further retries are made. You can replay individual events from the dashboard or via GET /v1/webhooks/:id/events.