Misar IO Docs

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.