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:
- Go to Settings > Webhooks
- Click "Add Webhook"
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- 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.