Webhooks Guide
Overview
Webhooks provide tenant-scoped event notifications over HTTP POST.
Each endpoint has:
- URL
- Secret (optional but recommended)
- Event filter list (
event_types) - Active/inactive state
Deliveries are persisted for observability and troubleshooting.
Supported Events
message.receivedmessage.sentmessage.deliveredmessage.bouncedmessage.rejectedmessage.complaineddomain.verified
Endpoint Management
Use /app/api/webhooks with X-Zmail-Session.
- Create:
POST /app/api/webhooks - List:
GET /app/api/webhooks - Get:
GET /app/api/webhooks/{id} - Update:
PATCH /app/api/webhooks/{id} - Delete:
DELETE /app/api/webhooks/{id}
Delivery History
GET /app/api/webhooks/{id}/deliveries?limit=100
Each delivery includes:
- Event type
- Attempt number
- HTTP status code
- Error text (if failed)
- Delivery timestamp
- Event ID
Test Event Endpoint
POST /app/api/webhooks/{id}/test- Body:
{ "event_type": "message.received" }
This sends a synthetic event to the selected webhook only.
Signature Headers
When a webhook secret is set, Zmail includes Svix-style headers:
svix-idsvix-timestampsvix-signature
Current signing format is v1,<base64-hmac-sha256> over:
<svix-id>.<svix-timestamp>.<raw_json_body>
Use constant-time comparison in your verifier.
Retry Behavior
- Up to 3 attempts per endpoint/event
- Backoff between attempts
- Every attempt is logged in
webhook_deliveries
Receiver Best Practices
- Return 2xx quickly; process asynchronously.
- Make handlers idempotent keyed by
event_id. - Log payload + headers for debugging.
- Keep endpoint availability and TLS healthy.
Example Receiver (Python/FastAPI)
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/webhooks/zmail")
async def zmail_webhook(request: Request):
payload = await request.json()
# TODO: verify signature headers before processing in production
# TODO: enqueue background job using payload["event_id"] for idempotency
return {"ok": True}