Skip to main content

Webhooks

mailbot sends HTTP POST requests to your endpoint when email events occur. You can use webhooks to trigger automations, update your database, or feed events into your AI agent.

Setting up webhooks

To register a webhook endpoint from the dashboard:

  1. Go to Settings > Webhooks
  2. Click "Add Webhook"
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Click Save

You can also register an endpoint via the API:

curl -X POST https://getmail.bot/v1/webhooks \
-H "Authorization: Bearer $MAILBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/mailbot",
"events": ["message.sent", "message.delivered", "message.opened", "message.clicked", "message.received", "message.bounced"]
}'

A test event is sent immediately on creation to verify your endpoint is reachable.

Verifying webhook signatures

All webhook requests are signed with HMAC-SHA256. Always verify signatures before processing events.

Signature construction:

HMAC-SHA256(webhook_secret, timestamp + "." + JSON.stringify(payload))

Headers sent with every request:

  • X-Mailbot-Signature: sha256=<hex>
  • X-Mailbot-Timestamp: <unix_seconds>

Node.js verification

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, timestamp, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + JSON.stringify(payload))
.digest('hex');

const expectedSig = 'sha256=' + expected;
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(signature)
);
}

// Usage in Express
app.post('/webhooks/mailbot', express.json(), (req, res) => {
const signature = req.headers['x-mailbot-signature'];
const timestamp = req.headers['x-mailbot-timestamp'];

if (!verifyWebhookSignature(req.body, signature, timestamp, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

// Process event
console.log('Event:', req.body.event);
res.status(200).send('OK');
});

Python verification

import hmac
import hashlib
import json

def verify_webhook_signature(payload: dict, signature: str, timestamp: str, secret: str) -> bool:
message = timestamp + "." + json.dumps(payload, separators=(",", ":"))
expected = "sha256=" + hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

# Usage in Flask
@app.route("/webhooks/mailbot", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Mailbot-Signature")
timestamp = request.headers.get("X-Mailbot-Timestamp")

if not verify_webhook_signature(request.json, signature, timestamp, WEBHOOK_SECRET):
return "Invalid signature", 401

# Process event
print("Event:", request.json["event"])
return "OK", 200

Event payload schemas

All payloads share this base structure:

{
"event": "<event_type>",
"timestamp": 1711100000,
"data": { ... }
}

message.sent

{
"event": "message.sent",
"timestamp": 1711100000,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "agent@mailbot.id",
"to": ["customer@gmail.com"],
"subject": "Your support request",
"status": "sent"
}
}

message.delivered

{
"event": "message.delivered",
"timestamp": 1711100010,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "agent@mailbot.id",
"to": ["customer@gmail.com"],
"subject": "Your support request",
"status": "delivered",
"deliveredAt": "2026-03-22T03:13:30Z"
}
}

message.opened

{
"event": "message.opened",
"timestamp": 1711100060,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "agent@mailbot.id",
"to": ["customer@gmail.com"],
"subject": "Your support request",
"openCount": 1,
"openedAt": "2026-03-22T03:14:00Z"
}
}

message.clicked

{
"event": "message.clicked",
"timestamp": 1711100120,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "agent@mailbot.id",
"to": ["customer@gmail.com"],
"subject": "Your support request",
"url": "https://your-app.com/verify?token=abc123",
"clickCount": 1,
"clickedAt": "2026-03-22T03:15:00Z"
}
}

message.received

{
"event": "message.received",
"timestamp": 1711100200,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "customer@gmail.com",
"to": ["agent@mailbot.id"],
"subject": "Re: Your support request",
"body": "Thanks for the quick reply!",
"headers": {
"messageId": "<uuid@mailbot.id>",
"inReplyTo": "<original-uuid@mailbot.id>",
"references": "<ref1> <ref2>"
}
}
}

message.bounced

{
"event": "message.bounced",
"timestamp": 1711100300,
"data": {
"messageId": "msg-uuid",
"threadId": "thread-uuid",
"inboxId": "inbox-uuid",
"from": "agent@mailbot.id",
"to": ["invalid@example.com"],
"subject": "Your support request",
"bounceType": "hard",
"bounceCode": "550",
"bounceMessage": "Mailbox not found",
"bouncedAt": "2026-03-22T03:18:00Z"
}
}

Retry policy

mailbot retries failed deliveries up to 3 times using exponential backoff:

  • Retry 1: 10 seconds after the initial failure
  • Retry 2: 60 seconds after the first retry
  • Retry 3: 300 seconds after the second retry

Your endpoint must return a 2xx status code within 30 seconds. After all retries are exhausted, the event is marked as failed and can be replayed from the dashboard.

Best practices

Respond immediately

Return 200 before processing the event. Use a queue or background job for any work that takes more than a few milliseconds. mailbot will retry if it does not receive a 2xx response within 30 seconds.

Always verify signatures

Never process unverified webhook payloads in production. Use the X-Mailbot-Signature and X-Mailbot-Timestamp headers with the verification functions shown above.

Handle duplicate events

Use the messageId as an idempotency key. Your handler should be safe to call multiple times with the same event, since retries can produce duplicate deliveries.

Use HTTPS endpoints only

mailbot will not deliver webhooks to HTTP endpoints. Your endpoint URL must begin with https://.

Monitor failures

Check the Webhooks section in your dashboard for failed deliveries. You can replay any failed event directly from the dashboard without needing to re-trigger the original action.