Skip to main content

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 TypeDescription
EMAIL_EVENTDelivery, opens, clicks, bounces, spam reports, and other email activity events
DOMAIN_VERIFICATIONNotifications when domain verification status changes

Email Events

EventDescriptionWhen Triggered
deliveryEmail successfully delivered to recipient's inboxRecipient's mail server accepts the email
openRecipient opened the emailTracking pixel is loaded (requires open tracking)
clickRecipient clicked a linkTracked link is clicked (requires click tracking)
bounceEmail bouncedDelivery failed permanently (hard) or temporarily (soft)
spamEmail marked as spamRecipient reports spam or spam filter triggers
unsubscribeRecipient unsubscribedUser clicks unsubscribe link
policy_rejectionEmail rejected by policyContent or sender violates policies
generation_failureEmail failed to generateTemplate error or rendering failure
generation_rejectionEmail generation rejectedContent validation failed
smtp_errorSMTP sending errorError during SMTP transmission
phishingPhishing detectedContent flagged as potential phishing
imap_errorIMAP monitoring errorError 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

FieldTypeRequiredDescription
webhook_urlstring✅ YesThe HTTPS URL to receive webhook requests. Must be publicly accessible.
eventstring✅ YesEvent type: "EMAIL_EVENT" or "DOMAIN_VERIFICATION"
methodstring✅ YesHTTP method: "GET", "POST", "PUT", "DELETE", "PATCH"
headersobjectNoCustom headers to include in webhook requests (e.g., authentication)
webhook_optionsobjectNoConfiguration for event filtering (only for EMAIL_EVENT)

Webhook Options (for EMAIL_EVENT)

FieldTypeDefaultDescription
events.deliverybooleanfalseReceive delivery notifications
events.openbooleanfalseReceive open tracking events
events.clickbooleanfalseReceive click tracking events
events.bouncebooleanfalseReceive bounce notifications
events.spambooleanfalseReceive spam report notifications
events.unsubscribebooleanfalseReceive unsubscribe notifications
events.policy_rejectionbooleanfalseReceive policy rejection notifications
events.generation_failurebooleanfalseReceive generation failure notifications
events.generation_rejectionbooleanfalseReceive generation rejection notifications
events.smtp_errorbooleanfalseReceive SMTP error notifications
events.phishingbooleanfalseReceive phishing detection notifications
events.imap_errorbooleanfalseReceive 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

ErrorCauseSolution
at least one of webhook_options.events must be trueNo events enabled for EMAIL_EVENTEnable at least one event type
invalid webhook_options.events for DOMAIN_VERIFICATIONEvents specified for domain webhookRemove webhook_options for DOMAIN_VERIFICATION
Webhook Already ExistsWebhook for this event type existsUpdate 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

FieldTypeRequiredDescription
eventstring✅ YesThe event type of the webhook to update
webhook_urlstringNoNew webhook URL (if changing)
methodstringNoNew HTTP method (if changing)
headersobjectNoNew headers (replaces existing)
webhook_optionsobjectNoNew 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

FieldTypeRequiredDescription
eventstring✅ YesThe 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 TypeDescription
hardPermanent failure - email address is invalid
softTemporary 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

IssueCauseSolution
Webhooks not receivedURL not accessibleEnsure endpoint is publicly accessible
422 Unprocessable EntityCannot connect to URLVerify URL is correct and server is running
Missing eventsEvents not enabledCheck webhook_options.events configuration
Duplicate eventsRetry logicImplement idempotency using ref_id
Authentication failedMissing/wrong headersVerify header configuration