Docs/Webhooks
Events

Webhooks

Webhooks let you receive real-time notifications when email delivery events occur - sent, delivered, failed, bounced, or complained. Each event is signed with HMAC-SHA256 so you can verify it came from SendFleet.

Webhooks are only available on Shared SES (Growth and Pro plans). BYOC sends route through your own AWS account - delivery events are available in your SES dashboard and CloudWatch directly. No email content is stored on our end for BYOC sends.

How webhooks work

When a delivery event occurs, SendFleet makes an HTTP POST to your endpoint URL with a JSON body describing the event. Your endpoint must respond with a 2xx status code within 5 seconds. Non-2xx responses are logged as failed deliveries - we do not currently retry failed webhook calls.

SES Event
SNS Notification
SendFleet
Process → Sign
Your Server
POST /your-webhook
Events are dispatched asynchronously in a background thread. Your endpoint must respond within 5 seconds. The thread is daemon-level - it will not block server shutdown, but it also won't retry on failure.

Creating a webhook endpoint

Go to Dashboard → Webhooks → Add endpoint. Enter a URL and select the event types you want to receive. A signing secret is generated and shown once - store it securely.

The signing secret is shown exactly once. Copy it immediately and store it in your environment variables or secrets manager. It cannot be retrieved again - if lost, delete the endpoint and create a new one.

Signature verification

Every webhook request includes an X-SendFleet-Signature header containing an HMAC-SHA256 hex digest of the request body, computed using your endpoint's signing secret. Always verify the signature before processing any event.

Signature format

Webhook request headers
POST /your-webhook HTTP/1.1
Content-Type: application/json
X-SendFleet-Signature: sha256=a3f2b1c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2
X-SendFleet-Event: email.delivered
User-Agent: SendFleet-Webhook/1.0

Verification examples

import hmac, hashlib
from django.http import HttpResponse

WEBHOOK_SECRET = "your_signing_secret"

def webhook_view(request):
    sig_header = request.META.get("HTTP_X_SENDFLEET_SIGNATURE", "")
    expected_sig = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        request.body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(sig_header, expected_sig):
        return HttpResponse(status=401)

    import json
    event = json.loads(request.body)
    event_type = event.get("event_type")

    if event_type == "email.delivered":
        # update your database, trigger follow-up, etc.
        pass

    return HttpResponse("OK")

Event types

Select one or more event types when creating an endpoint. Events are scoped per endpoint - you can have different endpoints for different event subsets.

Event typeTriggered when
email.sentEmail is accepted by SendFleet and placed in the delivery queue. Fired immediately on a successful /api/send/ call.
email.deliveredAWS SES confirms successful delivery to the recipient's mail server. Typically within seconds for healthy inboxes.
email.failedSES encountered a rendering error or configuration failure. The email was not sent.
email.bouncedThe email was rejected by the recipient's mail server. Hard bounces also add the address to the suppression list.
email.complainedThe recipient marked the email as spam. The address is added to the suppression list automatically.

Event payload reference

All events share a common envelope. The payload object differs slightly per event type.

email.sent

email.sent payload
{
  "event_type": "email.sent",
  "payload": {
    "message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "recipient":  "alice@example.com",
    "subject":    "Your order has shipped",
    "status":     "queued",
    "mode":       "shared"
  }
}

email.delivered

email.delivered payload
{
  "event_type": "email.delivered",
  "payload": {
    "message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "recipient":  "alice@example.com",
    "status":     "delivered"
  }
}

email.bounced

email.bounced payload
{
  "event_type": "email.bounced",
  "payload": {
    "message_id":  "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "recipient":   "alice@example.com",
    "bounce_type": "Permanent",
    "status":      "bounced"
  }
}

The bounce_type field mirrors the SES classification: "Permanent" (hard bounce - address suppressed) or "Transient" (soft bounce - address not suppressed).

email.complained

email.complained payload
{
  "event_type": "email.complained",
  "payload": {
    "message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "recipient":  "alice@example.com",
    "status":     "complaint"
  }
}

email.failed

email.failed payload
{
  "event_type": "email.failed",
  "payload": {
    "message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "recipient":  "alice@example.com",
    "status":     "failed",
    "reason":     "SES rendering failure: template not found"
  }
}

Endpoint requirements

RequirementDetail
ProtocolHTTPS or HTTP. HTTPS strongly recommended in production.
Response codeAny 2xx status (200, 201, 204, etc.). Non-2xx is logged as a failure.
Response timeMust respond within 5 seconds. Longer responses are treated as failures.
IdempotencyDesign your handler to be idempotent - the same event may be delivered more than once in rare cases.
Signature checkAlways verify X-SendFleet-Signature before processing. Reject unsigned or tampered payloads.

Best practices

1
Verify before processing
Check the X-SendFleet-Signature header first, before parsing the body or touching your database. Return 401 immediately on failure.
2
Respond fast, process async
Return 200 OK as quickly as possible, then process the event asynchronously (job queue, Celery task, etc.). Heavy processing inside the webhook handler risks timeouts.
3
Handle duplicates gracefully
Use the message_id field as an idempotency key. Store processed IDs in your database and skip re-processing if already seen.
4
Act on bounces and complaints
When you receive email.bounced (Permanent) or email.complained, mark the address as unsubscribed in your own database in addition to the SendFleet suppression list. This prevents you from re-adding addresses and hitting the same bounce again.

Managing endpoints

Go to Dashboard → Webhooks to view, activate, deactivate, or delete endpoints. Deactivating an endpoint stops delivery without deleting it - useful for maintenance windows. Deleting is permanent.