How webhook deliveries, retries, and auto-pause work
Understanding how do.dev delivers webhooks helps you build reliable integrations.
When an event is generated:
Every delivery is an HTTP POST with these characteristics:
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 15 seconds |
| User-Agent | DoDevWebhook/1.0 |
| Header | Example | Description |
|---|---|---|
X-DoDevWebhook-Signature | t=1700000000,v1=abc123... | HMAC-SHA256 signature for verification |
X-DoDevWebhook-Id | evt_a1b2c3d4-... | Unique event identifier |
X-DoDevWebhook-Timestamp | 1700000000 | Unix timestamp of the delivery attempt |
Content-Type | application/json | Always JSON |
User-Agent | DoDevWebhook/1.0 | Identifies do.dev webhook system |
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
}A delivery is considered successful when your endpoint responds with any 2xx status code within 15 seconds.
| Response | Result |
|---|---|
200 OK | Success |
201 Created | Success |
204 No Content | Success |
301, 302 | Failure (redirects are not followed) |
400, 401, 403, 404 | Failure (client error) |
500, 502, 503 | Failure (server error) |
| Timeout (> 15s) | Failure |
| Connection refused | Failure |
| DNS resolution failure | Failure |
Failed deliveries are retried up to 5 times with exponential backoff:
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1st (initial) | Immediate | 0 |
| 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.
If a webhook endpoint accumulates 10 consecutive delivery failures (across any events), it is automatically paused:
pauseReason is recorded (e.g., "Auto-paused after 10 consecutive delivery failures")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.
Your webhook handler should be idempotent — processing the same event twice should have the same effect as processing it once. This is important because:
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);
}The Webhooks page in your dashboard shows:
The Event Log shows:
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"Respond fast — Return 200 immediately, then process asynchronously. If your handler takes more than 15 seconds, the delivery will be marked as failed.
Use a queue — For complex processing, push events to your own queue (SQS, Redis, etc.) and process them asynchronously.
Be idempotent — Always check eventId to avoid processing duplicates.
Monitor failures — Watch for auto-paused endpoints in the dashboard. Repeated failures usually indicate a bug in your handler or an infrastructure issue.
Test regularly — Use the test endpoint to verify your handler is working before relying on it in production.