Webhooks Guide
Webhooks enable real-time notifications about email events, allowing your application to react immediately to deliveries, opens, clicks, bounces, and other email activities.
Overview
When email events occur (delivery, open, click, bounce, etc.), EDITH sends an HTTP request to your configured webhook endpoint with event details. This enables you to:
- Update email status in your database
- Trigger follow-up actions
- Build real-time analytics dashboards
- Handle bounces and unsubscribes automatically
- Process incoming emails
Webhook Event Types
EDITH supports two main webhook categories:
| Event Type | Description |
|---|---|
EMAIL_EVENT | Delivery, opens, clicks, bounces, spam reports, and other email activity events |
DOMAIN_VERIFICATION | Notifications when domain verification status changes |
Email Events
| Event | Description | When Triggered |
|---|---|---|
delivery | Email successfully delivered to recipient's inbox | Recipient's mail server accepts the email |
open | Recipient opened the email | Tracking pixel is loaded (requires open tracking) |
click | Recipient clicked a link | Tracked link is clicked (requires click tracking) |
bounce | Email bounced | Delivery failed permanently (hard) or temporarily (soft) |
spam | Email marked as spam | Recipient reports spam or spam filter triggers |
unsubscribe | Recipient unsubscribed | User clicks unsubscribe link |
policy_rejection | Email rejected by policy | Content or sender violates policies |
generation_failure | Email failed to generate | Template error or rendering failure |
generation_rejection | Email generation rejected | Content validation failed |
smtp_error | SMTP sending error | Error during SMTP transmission |
phishing | Phishing detected | Content flagged as potential phishing |
imap_error | IMAP monitoring error | Error in IMAP connection or processing |
Register a Webhook
Endpoint
POST /v1/webhook/register
Purpose
Creates a new webhook endpoint to receive email event notifications.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url | string | ✅ Yes | The HTTPS URL to receive webhook requests. Must be publicly accessible. |
event | string | ✅ Yes | Event type: "EMAIL_EVENT" or "DOMAIN_VERIFICATION" |
method | string | ✅ Yes | HTTP method: "GET", "POST", "PUT", "DELETE", "PATCH" |
headers | object | No | Custom headers to include in webhook requests (e.g., authentication) |
webhook_options | object | No | Configuration for event filtering (only for EMAIL_EVENT) |
Webhook Options (for EMAIL_EVENT)
| Field | Type | Default | Description |
|---|---|---|---|
events.delivery | boolean | false | Receive delivery notifications |
events.open | boolean | false | Receive open tracking events |
events.click | boolean | false | Receive click tracking events |
events.bounce | boolean | false | Receive bounce notifications |
events.spam | boolean | false | Receive spam report notifications |
events.unsubscribe | boolean | false | Receive unsubscribe notifications |
events.policy_rejection | boolean | false | Receive policy rejection notifications |
events.generation_failure | boolean | false | Receive generation failure notifications |
events.generation_rejection | boolean | false | Receive generation rejection notifications |
events.smtp_error | boolean | false | Receive SMTP error notifications |
events.phishing | boolean | false | Receive phishing detection notifications |
events.imap_error | boolean | false | Receive IMAP error notifications |
Important: For EMAIL_EVENT webhooks, at least one event type must be set to true.
Example Request - Email Events Webhook
curl -X POST https://api.edith.example.com/v1/webhook/register \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://api.yourcompany.com/webhooks/email-events",
"event": "EMAIL_EVENT",
"method": "POST",
"headers": {
"X-Webhook-Secret": "your-secret-key-for-verification",
"Authorization": "Bearer your-internal-token"
},
"webhook_options": {
"events": {
"delivery": true,
"open": true,
"click": true,
"bounce": true,
"spam": true,
"unsubscribe": true,
"smtp_error": true
}
}
}'
Example Request - Domain Verification Webhook
curl -X POST https://api.edith.example.com/v1/webhook/register \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://api.yourcompany.com/webhooks/domain-status",
"event": "DOMAIN_VERIFICATION",
"method": "POST",
"headers": {
"X-Webhook-Secret": "your-secret-key"
}
}'
Response
{
"success": true,
"message": "webhook created"
}
Validation Errors
| Error | Cause | Solution |
|---|---|---|
at least one of webhook_options.events must be true | No events enabled for EMAIL_EVENT | Enable at least one event type |
invalid webhook_options.events for DOMAIN_VERIFICATION | Events specified for domain webhook | Remove webhook_options for DOMAIN_VERIFICATION |
Webhook Already Exists | Webhook for this event type exists | Update or delete existing webhook |
Update a Webhook
Endpoint
PUT /v1/webhook/update
Purpose
Modifies an existing webhook configuration. You can update the URL, method, headers, or event filters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
event | string | ✅ Yes | The event type of the webhook to update |
webhook_url | string | No | New webhook URL (if changing) |
method | string | No | New HTTP method (if changing) |
headers | object | No | New headers (replaces existing) |
webhook_options | object | No | New event filters (replaces existing) |
Example Request
curl -X PUT https://api.edith.example.com/v1/webhook/update \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "EMAIL_EVENT",
"webhook_url": "https://api.yourcompany.com/webhooks/v2/email-events",
"webhook_options": {
"events": {
"delivery": true,
"open": true,
"click": true,
"bounce": true,
"spam": true,
"unsubscribe": true,
"policy_rejection": true,
"generation_failure": true,
"smtp_error": true
}
}
}'
Response
{
"success": true,
"message": "webhook updated"
}
Delete a Webhook
Endpoint
DELETE /v1/webhook/delete
Purpose
Removes a webhook configuration. Events will no longer be sent to this endpoint.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
event | string | ✅ Yes | The event type of the webhook to delete: "EMAIL_EVENT" or "DOMAIN_VERIFICATION" |
Example Request
curl -X DELETE https://api.edith.example.com/v1/webhook/delete \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "EMAIL_EVENT"
}'
Response
{
"success": true,
"message": "webhook deleted"
}
Webhook Payload Structure
When events occur, EDITH sends an HTTP request to your webhook URL with the event details.
Delivery Event Payload
{
"event": "delivery",
"timestamp": "2024-01-15T10:30:00Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "recipient@example.com",
"name": "John Doe"
},
"sender": {
"email": "sender@mail.yourcompany.com",
"name": "Your Company"
},
"subject": "Your Order Confirmation",
"campaign_id": "order-confirmation",
"mailer_id": "domain_01JC3BBW8S9YGX2VNKG5MD7BTA",
"custom_args": {
"order_id": "12345",
"user_id": "usr_67890"
}
}
Open Event Payload
{
"event": "open",
"timestamp": "2024-01-15T11:45:00Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "recipient@example.com"
},
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"ip_address": "192.168.1.1",
"geo_location": {
"country": "US",
"city": "New York"
},
"campaign_id": "order-confirmation",
"custom_args": {
"order_id": "12345"
}
}
Click Event Payload
{
"event": "click",
"timestamp": "2024-01-15T11:47:30Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "recipient@example.com"
},
"url": "https://yourcompany.com/orders/12345",
"link_name": "Track Order",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"campaign_id": "order-confirmation",
"custom_args": {
"order_id": "12345"
}
}
Bounce Event Payload
{
"event": "bounce",
"timestamp": "2024-01-15T10:31:00Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "invalid@example.com"
},
"bounce_type": "hard",
"bounce_category": "mailbox_does_not_exist",
"bounce_reason": "550 5.1.1 The email account does not exist",
"campaign_id": "newsletter-jan",
"custom_args": {}
}
| Bounce Type | Description |
|---|---|
hard | Permanent failure - email address is invalid |
soft | Temporary failure - mailbox full, server down, etc. |
Spam Event Payload
{
"event": "spam",
"timestamp": "2024-01-15T12:00:00Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "recipient@example.com"
},
"campaign_id": "newsletter-jan",
"custom_args": {}
}
Unsubscribe Event Payload
{
"event": "unsubscribe",
"timestamp": "2024-01-15T12:30:00Z",
"ref_id": "01JC3BBW8S9YGX2VNKG5MD7BTA",
"recipient": {
"email": "recipient@example.com"
},
"campaign_id": "newsletter-jan",
"custom_args": {}
}
Implementing Your Webhook Endpoint
Basic Express.js Example
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Verify webhook signature (recommended)
function verifySignature(req, secret) {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === expectedSignature;
}
app.post('/webhooks/email-events', (req, res) => {
// Verify the webhook is from EDITH
const webhookSecret = process.env.WEBHOOK_SECRET;
if (req.headers['x-webhook-secret'] !== webhookSecret) {
return res.status(401).json({ error: 'Unauthorized' });
}
const event = req.body;
switch (event.event) {
case 'delivery':
console.log(`Email delivered to ${event.recipient.email}`);
// Update database, trigger notifications, etc.
break;
case 'open':
console.log(`Email opened by ${event.recipient.email}`);
break;
case 'click':
console.log(`Link clicked: ${event.url}`);
break;
case 'bounce':
console.log(`Email bounced: ${event.bounce_reason}`);
// Remove invalid emails from your list
if (event.bounce_type === 'hard') {
// markEmailAsInvalid(event.recipient.email);
}
break;
case 'spam':
console.log(`Spam complaint from ${event.recipient.email}`);
// Add to suppression list
break;
case 'unsubscribe':
console.log(`Unsubscribed: ${event.recipient.email}`);
// Update subscription status
break;
default:
console.log(`Unknown event: ${event.event}`);
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);
Python Flask Example
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
@app.route('/webhooks/email-events', methods=['POST'])
def handle_webhook():
# Verify webhook secret
webhook_secret = os.environ.get('WEBHOOK_SECRET')
if request.headers.get('X-Webhook-Secret') != webhook_secret:
return jsonify({'error': 'Unauthorized'}), 401
event = request.json
event_type = event.get('event')
if event_type == 'delivery':
print(f"Delivered to {event['recipient']['email']}")
# Process delivery
elif event_type == 'bounce':
print(f"Bounced: {event['bounce_reason']}")
if event['bounce_type'] == 'hard':
# Handle hard bounce
pass
elif event_type == 'spam':
print(f"Spam complaint: {event['recipient']['email']}")
# Handle spam complaint
elif event_type == 'unsubscribe':
print(f"Unsubscribed: {event['recipient']['email']}")
# Handle unsubscribe
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Best Practices
1. Respond Quickly
Return a 200 OK response as quickly as possible. Process events asynchronously if heavy processing is needed.
// Good: Acknowledge immediately, process later
app.post('/webhooks/email-events', async (req, res) => {
res.status(200).json({ received: true });
// Process asynchronously
setImmediate(() => {
processEvent(req.body);
});
});
2. Implement Idempotency
Webhooks may be retried. Use the ref_id to deduplicate events.
const processedEvents = new Set();
function handleEvent(event) {
if (processedEvents.has(event.ref_id)) {
return; // Already processed
}
processedEvents.add(event.ref_id);
// Process the event
}
3. Verify Webhook Authenticity
Always verify that webhooks are from EDITH using the secret header or signature.
if (req.headers['x-webhook-secret'] !== process.env.WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}
4. Handle Retries Gracefully
If your endpoint returns a non-2xx status, EDITH will retry the webhook. Ensure your handler is idempotent.
5. Log All Events
Keep detailed logs for debugging and auditing.
app.post('/webhooks/email-events', (req, res) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event: req.body.event,
ref_id: req.body.ref_id,
recipient: req.body.recipient?.email
}));
// ... handle event
});
6. Monitor Webhook Health
Track webhook success/failure rates and set up alerts for failures.
7. Use HTTPS
Always use HTTPS endpoints to ensure webhook payloads are encrypted in transit.
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Webhooks not received | URL not accessible | Ensure endpoint is publicly accessible |
| 422 Unprocessable Entity | Cannot connect to URL | Verify URL is correct and server is running |
| Missing events | Events not enabled | Check webhook_options.events configuration |
| Duplicate events | Retry logic | Implement idempotency using ref_id |
| Authentication failed | Missing/wrong headers | Verify header configuration |
Related Endpoints
- Send Email - Set custom_args for webhook context
- Email Logs - Query historical event data
- Inbound Email - Webhook configuration for incoming emails