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.
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.
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.
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
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 type | Triggered when |
|---|---|
| email.sent | Email is accepted by SendFleet and placed in the delivery queue. Fired immediately on a successful /api/send/ call. |
| email.delivered | AWS SES confirms successful delivery to the recipient's mail server. Typically within seconds for healthy inboxes. |
| email.failed | SES encountered a rendering error or configuration failure. The email was not sent. |
| email.bounced | The email was rejected by the recipient's mail server. Hard bounces also add the address to the suppression list. |
| email.complained | The 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
{
"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
{
"event_type": "email.delivered",
"payload": {
"message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
"recipient": "alice@example.com",
"status": "delivered"
}
}email.bounced
{
"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
{
"event_type": "email.complained",
"payload": {
"message_id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
"recipient": "alice@example.com",
"status": "complaint"
}
}email.failed
{
"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
| Requirement | Detail |
|---|---|
| Protocol | HTTPS or HTTP. HTTPS strongly recommended in production. |
| Response code | Any 2xx status (200, 201, 204, etc.). Non-2xx is logged as a failure. |
| Response time | Must respond within 5 seconds. Longer responses are treated as failures. |
| Idempotency | Design your handler to be idempotent - the same event may be delivered more than once in rare cases. |
| Signature check | Always verify X-SendFleet-Signature before processing. Reject unsigned or tampered payloads. |
Best practices
X-SendFleet-Signature header first, before parsing the body or touching your database. Return 401 immediately on failure.200 OK as quickly as possible, then process the event asynchronously (job queue, Celery task, etc.). Heavy processing inside the webhook handler risks timeouts.message_id field as an idempotency key. Store processed IDs in your database and skip re-processing if already seen.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.