Webhooks¶
FrontEngine webhooks deliver real-time notifications about email events to your application. Use them to track deliveries, handle bounces, and measure engagement.
Overview¶
When an event occurs (email delivered, bounced, opened, etc.), FrontEngine sends an HTTP POST request to your configured webhook URL with a JSON payload describing the event.
Supported Events¶
Message Events:
| Event | Trigger |
|---|---|
MessageSent | Message successfully sent to a recipient |
MessageDelayed | Delivery failed temporarily, will be retried |
MessageDeliveryFailed | Message could not be delivered (permanent failure) |
MessageHeld | Message was held for review |
MessageBounced | A bounce notification was received for a previously accepted message |
MessageLinkClicked | A recipient clicked a link in the email |
Server Events:
| Event | Trigger |
|---|---|
SendLimitApproaching | Server has reached 90% of sending capacity |
SendLimitExceeded | Server has exceeded its sending limit |
DomainDNSError | SPF, DKIM, MX, or return path DNS validation failed for a domain |
Configuration¶
Setting Up a Webhook¶
- Go to Services > Transactional Email > Webhooks
- Click Add Webhook
- Configure:
- URL: Your HTTPS endpoint (e.g.,
https://app.example.com/webhooks/email) - Events: Select which events to receive
- Secret: A shared secret for payload verification
- URL: Your HTTPS endpoint (e.g.,
- Click Save
HTTPS Required
Webhook URLs must use HTTPS. HTTP endpoints are not supported for security reasons.
Verifying Webhook Signatures¶
Every webhook request includes a signature header for payload verification:
Verify the signature in your application:
import hashlib
import hmac
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
Payload Format¶
All webhook payloads are JSON. The structure varies by event type.
MessageSent Payload¶
{
"event": "MessageSent",
"payload": {
"status": "sent",
"details": "Message accepted by remote server",
"output": "250 OK",
"time": 1.23,
"sent_with_ssl": true,
"timestamp": 1711806600,
"message": {
"id": 12345,
"token": "abc123def456",
"direction": "outgoing",
"message_id": "<[email protected]>",
"to": "[email protected]",
"from": "[email protected]",
"subject": "Order Confirmation #12345",
"timestamp": 1711806590,
"spam_status": "NotSpam",
"tag": "order-confirmation"
}
}
}
MessageBounced Payload¶
Includes both the original message and the bounce details:
{
"event": "MessageBounced",
"payload": {
"original_message": {
"id": 12345,
"token": "abc123def456",
"to": "[email protected]",
"from": "[email protected]",
"subject": "Order Confirmation"
},
"bounce": {
"id": 67890,
"token": "xyz789",
"to": "[email protected]",
"from": "[email protected]",
"subject": "Undelivered Mail Returned to Sender"
}
}
}
MessageLinkClicked Payload¶
{
"event": "MessageLinkClicked",
"payload": {
"url": "https://example.com/track-order/12345",
"token": "abc123def456",
"ip_address": "198.51.100.42",
"user_agent": "Mozilla/5.0...",
"message": {
"id": 12345,
"token": "abc123def456",
"to": "[email protected]",
"from": "[email protected]",
"subject": "Order Confirmation #12345",
"tag": "order-confirmation"
}
}
}
SendLimitApproaching / SendLimitExceeded Payload¶
{
"event": "SendLimitApproaching",
"payload": {
"server": {
"uuid": "server-uuid",
"name": "My Mail Server",
"permalink": "my-mail-server",
"organization": "My Organization"
},
"volume": 9000,
"limit": 10000
}
}
DomainDNSError Payload¶
{
"event": "DomainDNSError",
"payload": {
"domain": "example.com",
"uuid": "domain-uuid",
"dns_checked_at": "2026-03-30T14:00:00Z",
"spf_status": "OK",
"spf_error": null,
"dkim_status": "Missing",
"dkim_error": "No DKIM record found for selector",
"mx_status": "OK",
"mx_error": null,
"return_path_status": "OK",
"return_path_error": null,
"server": {
"uuid": "server-uuid",
"name": "My Mail Server"
}
}
}
Best Practices¶
Respond Quickly¶
Your endpoint should return a 200 status within 5 seconds. Process webhook data asynchronously — enqueue the payload and return immediately.
@app.post("/webhooks/email")
async def handle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return Response(status_code=401)
# Enqueue for async processing
await queue.put(payload)
return Response(status_code=200)
Handle Retries¶
If your endpoint returns a non-2xx status or times out, FrontEngine retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 12 hours |
After 5 failed attempts, the webhook is marked as failed. You can view and replay failed webhooks in the portal.
Deduplicate Events¶
Webhook deliveries are at least once. Your endpoint may receive the same event more than once. Use the message_id and event fields to deduplicate.
Idempotency
Design your webhook handler to be idempotent — processing the same event twice should produce the same result as processing it once.
Testing Webhooks¶
Use the Test button in the portal to send a sample event to your endpoint. You can also use tools like webhook.site during development to inspect payloads.
Next Steps¶
- API Reference — Send messages via the REST API
- Authentication — Configure SPF, DKIM, and DMARC