Skip to main content

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

SettingRecommendation
Endpoint URLStable production URL. Not a tunnel or local domain.
Signing secretRotate quarterly. Use webhooks.security API to manage.
EventsSubscribe only to events you process. Subscribe to delivery, bounce, complaint at minimum.
FilterUse metadata filter if you have multiple webhooks per account.
Timeout budgetImplicit 5 seconds. Acknowledge well under this.

Debugging delivery failures

Symptom: webhook events stop arriving

Check:

  1. Endpoint reachable from public internet (curl from a different network).
  2. Endpoint returns 2xx on a synthetic test (POST /v1/webhooks/{id}/test).
  3. SSL certificate valid (Mailtarget rejects self-signed certs).
  4. 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:

  1. Open the campaign in the dashboard.
  2. Find the delivery log; failed webhook deliveries are listed with their error.
  3. Click Replay on each event, or use the bulk replay action.
  4. 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.