Webhooks
Webhooks are the primary mechanism for receiving real-time notifications from EmailEngine about mailbox events, message changes, and delivery status. Instead of repeatedly polling for updates, EmailEngine pushes notifications to your application as events occur.
Why Use Webhooks?
Real-Time Updates
- Instant notifications when events occur
- No polling delays or missed events
- Process messages as they arrive
Efficient
- Eliminates the need for constant polling
- Reduces API calls and server load
- Lower latency for time-sensitive operations
Comprehensive Event Coverage
- Message lifecycle (new, updated, deleted)
- Delivery status (sent, failed, bounced)
- Account status (connected, disconnected, errors)
- User interactions (opens, clicks, unsubscribes)
Scalable
- Handle high message volumes effortlessly
- Process events asynchronously
- Built on BullMQ for reliability
Setting Up Webhooks
1. Configure Webhook URL
Set your webhook endpoint URL in EmailEngine:
Via Web UI:
- Navigate to Configuration → Webhooks
- Check Enable webhooks
- Enter your Webhook URL:
https://your-app.com/webhooks/emailengine - Select which events to receive
- Click Update Settings
Via API:
Use the settings API to configure webhooks:
curl -X POST "https://your-emailengine.com/v1/settings" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhooks": "https://your-app.com/webhooks/emailengine",
"webhooksEnabled": true,
"notifyHeaders": ["List-ID", "X-Priority"],
"notifyTextSize": 65536,
"notifyWebSafeHtml": true,
"notifyCalendarEvents": true
}'
2. Create Webhook Handler
Your webhook endpoint must:
- Accept HTTP POST requests
- Return a 2xx status code quickly (within 5 seconds)
- Process events asynchronously
Node.js Example:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/emailengine', async (req, res) => {
const event = req.body;
// Acknowledge receipt immediately
res.status(200).json({ success: true });
// Process asynchronously
processEvent(event).catch(err => {
console.error('Webhook processing error:', err);
});
});
async function processEvent(event) {
console.log(`Received ${event.event} event for account ${event.account}`);
switch (event.event) {
case 'messageNew':
await handleNewMessage(event);
break;
case 'messageSent':
await handleMessageSent(event);
break;
case 'messageFailed':
await handleMessageFailed(event);
break;
// Handle other events...
}
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Python Example:
from flask import Flask, request, jsonify
import asyncio
app = Flask(__name__)
@app.route('/webhooks/emailengine', methods=['POST'])
def webhook_handler():
event = request.get_json()
# Acknowledge immediately
asyncio.create_task(process_event(event))
return jsonify({'success': True}), 200
async def process_event(event):
event_type = event.get('event')
account = event.get('account')
print(f"Processing {event_type} for {account}")
if event_type == 'messageNew':
await handle_new_message(event)
elif event_type == 'messageSent':
await handle_message_sent(event)
# Handle other events...
if __name__ == '__main__':
app.run(port=3000)
PHP Example:
<?php
// webhook.php
// Read the webhook payload
$payload = file_get_contents('php://input');
$event = json_decode($payload, true);
// Respond immediately
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
// Close connection and process asynchronously
fastcgi_finish_request();
// Process the event
processEvent($event);
function processEvent($event) {
$eventType = $event['event'] ?? '';
$account = $event['account'] ?? '';
error_log("Processing $eventType for $account");
switch ($eventType) {
case 'messageNew':
handleNewMessage($event);
break;
case 'messageSent':
handleMessageSent($event);
break;
// Handle other events...
}
}
?>
Webhook Events
EmailEngine sends different types of events organized into categories:
Message Events
messageNew
Triggered when a new message is detected in a mailbox folder.
Payload Example:
{
"account": "example",
"event": "messageNew",
"data": {
"id": "AAAAAQAAAeE",
"uid": 12345,
"path": "INBOX",
"emailId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"threadId": "1743d29c-b67d-4747-9016-b8850a5a39bd",
"date": "2025-10-13T10:23:45.000Z",
"flags": ["\\Seen"],
"labels": ["\\Inbox"],
"unseen": false,
"flagged": false,
"answered": false,
"draft": false,
"size": 45678,
"subject": "Meeting Tomorrow",
"from": {
"name": "John Doe",
"address": "john@example.com"
},
"to": [
{
"name": "Jane Smith",
"address": "jane@company.com"
}
],
"messageId": "<abc123@example.com>",
"inReplyTo": "<xyz789@example.com>",
"text": "Plain text body...",
"html": ["<p>HTML body...</p>"],
"attachments": [
{
"id": "AAAAAgAAAeEBAAAAAQAAAeE",
"contentType": "application/pdf",
"encodedSize": 45000,
"filename": "report.pdf",
"embedded": false
}
]
}
}
Use Cases:
- Trigger support ticket creation
- Process incoming orders
- Filter and classify emails
- Feed emails to AI analysis
messageDeleted
Triggered when a message is removed from a folder.
Payload Example:
{
"account": "example",
"event": "messageDeleted",
"data": {
"id": "AAAAAQAAAeE",
"uid": 12345,
"path": "INBOX",
"emailId": "1743d29c-b67d-4747-9016-b8850a5a39bd"
}
}
Use Cases:
- Sync deletions to external systems
- Update indexes and databases
- Track user behavior
messageUpdated
Triggered when message flags or labels change.
Payload Example:
{
"account": "example",
"event": "messageUpdated",
"data": {
"id": "AAAAAQAAAeE",
"uid": 12345,
"path": "INBOX",
"changes": {
"flags": {
"added": ["\\Seen"],
"removed": [],
"value": ["\\Seen", "\\Flagged"]
},
"labels": {
"added": ["\\Important"],
"removed": ["\\Inbox"],
"value": ["\\Important", "\\Starred"]
}
}
}
}
Payload Fields:
changes.flags.added- Flags that were just addedchanges.flags.removed- Flags that were just removedchanges.flags.value- Complete current flag listchanges.labels.added- Labels added (Gmail only)changes.labels.removed- Labels removed (Gmail only)changes.labels.value- Complete current label list (Gmail only)
Use Cases:
- Track read status
- Sync flags to external systems
- Monitor user actions
Delivery Events
messageSent
Triggered when a queued message is successfully accepted by the SMTP server.
Payload Example:
{
"account": "example",
"event": "messageSent",
"data": {
"queueId": "abc123def456",
"messageId": "<sent-message@example.com>",
"response": "250 2.0.0 OK: queued as 1234ABCD"
}
}
messageDeliveryError
Triggered when delivery to the SMTP server fails temporarily (will retry).
Payload Example:
{
"account": "example",
"event": "messageDeliveryError",
"data": {
"queueId": "abc123def456",
"error": "Connection timeout",
"response": "421 4.4.2 Connection timeout"
}
}
messageFailed
Triggered when delivery permanently fails (no more retries).
Payload Example:
{
"account": "example",
"event": "messageFailed",
"data": {
"queueId": "abc123def456",
"error": "Recipient rejected",
"response": "550 5.1.1 User unknown"
}
}
messageBounce
Triggered when a bounce message is received.
Payload Example:
{
"account": "example",
"event": "messageBounce",
"data": {
"id": "AAAAAQAAAeE",
"bounceMessage": "Mailbox full",
"recipient": "user@example.com",
"action": "failed",
"status": "5.2.2"
}
}
messageComplaint
Triggered when a feedback loop complaint is detected.
Payload Example:
{
"account": "example",
"event": "messageComplaint",
"data": {
"id": "AAAAAQAAAeE",
"recipient": "user@example.com",
"complaintType": "abuse"
}
}
Mailbox Events
mailboxNew
Triggered when a new folder is created.
mailboxDeleted
Triggered when a folder is deleted.
mailboxReset
Triggered when a folder's UIDVALIDITY changes (rare event).
Account Events
accountAdded
Triggered when a new account is registered.
accountInitialized
Triggered when initial sync completes for an account.
accountDeleted
Triggered when an account is removed.
authenticationSuccess
Triggered when account successfully authenticates.
authenticationError
Triggered when authentication fails.
connectError
Triggered when connection to email server fails.
Tracking Events
trackOpen
Triggered when a recipient opens an email (requires open tracking enabled).
Payload Example:
{
"account": "example",
"event": "trackOpen",
"date": "2025-10-13T10:30:00.000Z",
"data": {
"messageId": "<sent-message@example.com>",
"remoteAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
}
}
Note: The timestamp is in the parent-level date field, not in data.
trackClick
Triggered when a recipient clicks a tracked link.
Payload Example:
{
"account": "example",
"event": "trackClick",
"date": "2025-10-13T10:31:00.000Z",
"data": {
"messageId": "<sent-message@example.com>",
"url": "https://example.com/product",
"remoteAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
}
}
Note: The timestamp is in the parent-level date field, not in data.
listUnsubscribe
Triggered when a recipient unsubscribes.
listSubscribe
Triggered when a recipient resubscribes.
Testing Webhooks
Using webhook.site
The easiest way to test webhooks is using a temporary webhook inspector:
- Visit https://webhook.site/
- Copy your unique webhook URL
- Set it as the webhook URL in EmailEngine
- Trigger an event (send a test email, add an account, etc.)
- View the webhook payload in real-time
Tailing Webhooks to a Log File
For ongoing webhook monitoring, you can log all webhooks to a file and tail them:
Step 1: Create log file
sudo touch /var/log/emailengine-webhooks.log
sudo chown www-data /var/log/emailengine-webhooks.log # Adjust user as needed
Step 2: Create PHP webhook logger
<?php
// webhook-logger.php
$logFile = '/var/log/emailengine-webhooks.log';
// Read webhook payload
$payload = file_get_contents('php://input');
$headers = getallheaders();
// Create log entry
$logEntry = [
'timestamp' => date('c'),
'method' => $_SERVER['REQUEST_METHOD'],
'headers' => $headers,
'request' => json_decode($payload, true)
];
// Append to log file
file_put_contents($logFile, json_encode($logEntry) . "\n", FILE_APPEND);
// Respond
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['success' => true]);
?>
Step 3: Tail the log with jq
# Install jq if needed
sudo apt update && sudo apt install -y jq
# Tail and pretty-print webhooks
tail -f /var/log/emailengine-webhooks.log | jq
This gives you a real-time, pretty-printed view of all incoming webhooks.
Send Test Webhook
EmailEngine allows you to send a test webhook from the UI:
- Go to Configuration → Webhooks
- Click Send Test Payload
- Check your webhook endpoint receives the test
Debugging Webhooks
If webhooks aren't working as expected, follow this diagnostic process:
1. Verify External Connectivity
Test with webhook.site:
- Set webhook URL to https://webhook.site/your-unique-id
- Trigger an event in EmailEngine
- Check if webhook.site receives the request
If no request appears:
- Check firewall rules
- Verify DNS resolution
- Ensure EmailEngine can make outbound HTTPS requests
- Check for typos in webhook URL
2. Monitor Webhook Queue
EmailEngine uses BullMQ to manage webhook delivery. To inspect webhook jobs:
- Go to Tools → Bull Board
- Select Webhooks queue
- Check these tabs:
- Active: Currently processing
- Delayed: Failed but will retry
- Failed: Exceeded retry limit
- Completed: Successfully delivered (if retention enabled)
Enable Job Retention:
To keep failed jobs for inspection:
- Go to Configuration → Service
- Set Completed/failed queue entries to keep to 100
- Save changes
Now failed webhooks remain visible in Bull Board with full error details.
3. Inspect Failed Jobs
Click on a failed job in Bull Board to see:
- Complete error stack trace
- Request headers sent
- Payload data
- Response from your server
- Retry attempts
Common errors:
- Connection timeout: Your server is unreachable
- SSL/TLS error: Certificate issues
- 4xx status: Your server rejected the webhook
- 5xx status: Your server had an internal error
- JSON parse error: Your server returned invalid JSON
4. Verify Event Generation
Test if events are being generated at all:
Add a new account:
- Should trigger
accountAddedandaccountInitializedevents
Send test email to an account:
- Should trigger
messageNewwithin 10-60 seconds - If not, verify message arrived (check via webmail)
- Check message is visible via API:
curl "https://your-emailengine.com/v1/account/ACCOUNT_ID/messages?path=INBOX" \
-H "Authorization: Bearer TOKEN"
If message is missing:
- Wrong account credentials
- OAuth token lacks required scopes
- Message filtered to different folder
5. Special Requirements for API Backends
Gmail API + Cloud Pub/Sub
If using Gmail API (not IMAP):
- Go to Configuration → OAuth2
- Select your Gmail OAuth app
- Scroll to Cloud Pub/Sub configuration
- Verify all show Created (in green):
- Topic
- Subscription
- Gmail bindings
If not created:
- Google Cloud service account missing IAM roles
- Pub/Sub API not enabled
- Invalid credentials
Microsoft Graph API
If using MS Graph (not IMAP):
- Go to Email Accounts
- Select the account
- Scroll to Change subscription
- Verify status is Created and expiration is in future
If not created:
- EmailEngine not reachable from Microsoft servers
- TLS certificate invalid
- Service URL not configured correctly
- OAuth app missing required scopes
Microsoft Graph requires these endpoints to be publicly accessible:
https://YOUR-EMAILENGINE-HOST/oauth/msg/lifecycle
https://YOUR-EMAILENGINE-HOST/oauth/msg/notification
Webhook Security
Verify Webhook Authenticity
EmailEngine can sign webhooks using HMAC:
1. Set a service secret using the settings API:
curl -X POST "https://your-emailengine.com/v1/settings" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"serviceSecret": "your-secret-key-here"}'
2. Verify signature in your handler:
The signature is computed on the raw request body using HMAC-SHA256 and encoded as base64url.
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64url');
return signature === expectedSignature;
}
// Important: Use raw body parser to get the exact bytes for signature verification
app.post('/webhooks/emailengine', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-ee-wh-signature'];
const secret = process.env.SERVICE_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse body after verification
const event = JSON.parse(req.body.toString());
// Process webhook...
res.json({ success: true });
});
Use HTTPS
Always use HTTPS for webhook URLs to prevent:
- Man-in-the-middle attacks
- Credential exposure
- Data tampering
Implement Rate Limiting
Protect your webhook endpoint:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // Max 100 requests per minute
message: 'Too many webhook requests'
});
app.post('/webhooks/emailengine', webhookLimiter, handleWebhook);