Delivery & Retries

How webhook deliveries, retries, and auto-pause work

Understanding how do.dev delivers webhooks helps you build reliable integrations.

Delivery Flow

When an event is generated:

  1. The event is stored in your event log (immediately queryable)
  2. Your webhook endpoints are checked for matching event filters
  3. For each matching endpoint, a delivery is enqueued
  4. The delivery worker sends an HTTP POST to your URL with the signed payload
  5. The delivery result (success or failure) is recorded

Request Format

Every delivery is an HTTP POST with these characteristics:

PropertyValue
MethodPOST
Content-Typeapplication/json
Timeout15 seconds
User-AgentDoDevWebhook/1.0

Headers

HeaderExampleDescription
X-DoDevWebhook-Signaturet=1700000000,v1=abc123...HMAC-SHA256 signature for verification
X-DoDevWebhook-Idevt_a1b2c3d4-...Unique event identifier
X-DoDevWebhook-Timestamp1700000000Unix timestamp of the delivery attempt
Content-Typeapplication/jsonAlways JSON
User-AgentDoDevWebhook/1.0Identifies do.dev webhook system

Body

The request body is a JSON object with the event data:

{
  "eventId": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "send.email.sent",
  "service": "send",
  "data": { ... },
  "livemode": true,
  "createdAt": 1700000000000
}

Success Criteria

A delivery is considered successful when your endpoint responds with any 2xx status code within 15 seconds.

ResponseResult
200 OKSuccess
201 CreatedSuccess
204 No ContentSuccess
301, 302Failure (redirects are not followed)
400, 401, 403, 404Failure (client error)
500, 502, 503Failure (server error)
Timeout (> 15s)Failure
Connection refusedFailure
DNS resolution failureFailure

Retry Schedule

Failed deliveries are retried up to 5 times with exponential backoff:

AttemptDelayCumulative Time
1st (initial)Immediate0
2nd (retry 1)1 minute~1 minute
3rd (retry 2)5 minutes~6 minutes
4th (retry 3)30 minutes~36 minutes
5th (retry 4)2 hours~2.5 hours

After all 5 attempts are exhausted, the delivery is marked as expired.

Each retry sends the exact same payload with the same eventId, so your handler should be idempotent.

Auto-Pause

If a webhook endpoint accumulates 10 consecutive delivery failures (across any events), it is automatically paused:

  • No further events are delivered to the endpoint
  • The endpoint shows a "Paused" status in the dashboard
  • A pauseReason is recorded (e.g., "Auto-paused after 10 consecutive delivery failures")

Unpausing

To resume delivery, unpause the endpoint from the dashboard. This resets the failure counter to 0.

A successful delivery at any point also resets the consecutive failure counter. So if attempt 4 of an event succeeds, the counter resets even if the previous 3 attempts failed.

Idempotency

Your webhook handler should be idempotent — processing the same event twice should have the same effect as processing it once. This is important because:

  • Retries may deliver the same event multiple times
  • Network issues may cause duplicate deliveries in rare cases

Use the eventId field to track which events you've already processed:

async function handleWebhook(event: WebhookEvent) {
  // Check if we've already processed this event
  const existing = await db.getProcessedEvent(event.eventId);
  if (existing) {
    return; // Already processed, skip
  }

  // Process the event
  await processEvent(event);

  // Mark as processed
  await db.markEventProcessed(event.eventId);
}

Monitoring

Dashboard

The Webhooks page in your dashboard shows:

  • All configured endpoints with their status
  • Consecutive failure count per endpoint
  • Last delivery timestamp and HTTP status
  • Auto-pause banners with unpause button

The Event Log shows:

  • All events across services (filterable)
  • Expandable event details with full JSON payload
  • Delivery attempts per event (status, HTTP code, response time)

API

Use the Events API to programmatically monitor delivery status:

# Get a specific event with delivery results
curl "https://api.do.dev/v1/events/evt_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"

Best Practices

  1. Respond fast — Return 200 immediately, then process asynchronously. If your handler takes more than 15 seconds, the delivery will be marked as failed.

  2. Use a queue — For complex processing, push events to your own queue (SQS, Redis, etc.) and process them asynchronously.

  3. Be idempotent — Always check eventId to avoid processing duplicates.

  4. Monitor failures — Watch for auto-paused endpoints in the dashboard. Repeated failures usually indicate a bug in your handler or an infrastructure issue.

  5. Test regularly — Use the test endpoint to verify your handler is working before relying on it in production.