Webhook Reliability
Webhook reliability is the difference between knowing what happened to your email and guessing. A flaky endpoint loses delivery confirmations, bounces, and complaints. This page is the production checklist.
Endpoint design
1. Acknowledge fast
Respond 200 OK within 5 seconds. Do not run database writes, CRM API calls, or anything network-bound inside the handler.
def handle_webhook(request):
verify_signature_or_reject(request)
enqueue_for_async_processing(request.json)
return Response(status=200)
The async worker handles the heavy work. If it fails, retry from the queue. The webhook ack is decoupled from your downstream reliability.
2. Be idempotent
Mailtarget retries on non-2xx response or timeout. Same event delivered more than once is the norm during retry storms. Dedupe at the receiver.
key = f"{event['transmissionId']}:{event['event']}:{event['timestamp']}"
if not dedupe_table.insert_unique(key):
return Response(status=200) # already processed
do_the_work(event)
Use a unique constraint on the dedupe table. Insert-or-skip is faster than read-then-write.
3. Verify the signature with constant-time comparison
import hmac, hashlib
def verify(body_bytes, header_signature, secret):
expected = hmac.new(secret.encode(), body_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header_signature)
Constant-time compare prevents timing attacks. A regular == leaks the secret to an attacker who can spam your endpoint and measure response time.
Retry policy
Mailtarget retries on:
- Non-2xx response.
- Timeout (no response within the budget).
- Connection refused or DNS failure.
Retry pattern: exponential backoff over multiple attempts spread across hours. After the retry window expires, the event drops from the active queue and shows in the dashboard delivery log for manual replay.
Two consequences:
- A 30-second handler triggers a retry after the timeout. Acknowledge fast even if processing is slow.
- A flaky endpoint produces duplicate events as Mailtarget retries until acknowledgement succeeds. Idempotency catches duplicates.
Configuration
| Setting | Recommendation |
|---|---|
| Endpoint URL | Stable production URL. Not a tunnel or local domain. |
| Signing secret | Rotate quarterly. Use webhooks.security API to manage. |
| Events | Subscribe only to events you process. Subscribe to delivery, bounce, complaint at minimum. |
| Filter | Use metadata filter if you have multiple webhooks per account. |
| Timeout budget | Implicit 5 seconds. Acknowledge well under this. |
Debugging delivery failures
Symptom: webhook events stop arriving
Check:
- Endpoint reachable from public internet (curl from a different network).
- Endpoint returns 2xx on a synthetic test (
POST /v1/webhooks/{id}/test). - SSL certificate valid (Mailtarget rejects self-signed certs).
- Cloudflare or WAF not blocking the source IP.
Symptom: receiving every event twice or more
- Endpoint slow. Timeout triggers retry. Speed up the handler.
- Endpoint returning non-2xx intermittently. Check status code distribution in your logs.
- Dedupe missing. Add idempotency key as described above.
Symptom: signature verification fails intermittently
- Reverse proxy (nginx, Cloudflare) modifying the body. Match against the raw body before any transformation.
- Reverse proxy lowercasing or stripping the signature header. Read the raw header from the request.
- Wrong secret. Signing secret rotated but old secret still in your handler config.
Replay flow
If events drop from the queue:
- Open the campaign in the dashboard.
- Find the delivery log; failed webhook deliveries are listed with their error.
- Click Replay on each event, or use the bulk replay action.
- Confirm your endpoint received and processed the replayed events.
For programmatic replay, the API exposes a webhook batch-status endpoint. See /v1/webhooks/{id}/batch-status in the API Reference.
Related
- Webhooks for event types and payload shape.
- Webhook to CRM for an integration example.
- Errors and Rate Limits for the error catalog.