Skip to content

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

  1. Go to Services > Transactional Email > Webhooks
  2. Click Add Webhook
  3. 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
  4. 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:

X-Webhook-Signature: sha256=a1b2c3d4e5f6...

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