Delivery & Retries
How HelloJohn delivers webhook events, handles failures, and retries failed deliveries.
Delivery & Retries
HelloJohn delivers webhook events reliably using an automatic retry system with exponential backoff. This page explains delivery behavior, retry logic, and how to handle failures.
Delivery Behavior
When an event occurs:
- HelloJohn serializes the event payload
- Makes a
POSTrequest to your endpoint within 1 second - Waits up to 30 seconds for a response
- If
2xxresponse received → delivery marked succeeded - Otherwise → delivery marked failed, retry scheduled
Request Format
Every delivery is a POST request with these headers:
POST /webhooks/hellojohn HTTP/1.1
Content-Type: application/json
HelloJohn-Signature: sha256=abc123...
HelloJohn-Delivery-ID: del_01HABCDEF888001
HelloJohn-Event: user.created
HelloJohn-Timestamp: 1705399200| Header | Description |
|---|---|
HelloJohn-Signature | HMAC-SHA256 signature for verification |
HelloJohn-Delivery-ID | Unique ID for this delivery attempt |
HelloJohn-Event | The event type |
HelloJohn-Timestamp | Unix timestamp when the event was created |
Retry Schedule
If delivery fails, HelloJohn retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
After 5 failed attempts, the delivery is marked permanently failed. You can manually retry it from the dashboard or API.
Idempotency
Your endpoint may receive the same event more than once (due to retries or network issues). Design your handler to be idempotent — processing the same event twice has the same result as processing it once:
app.post("/webhooks/hellojohn", async (req, res) => {
const event = JSON.parse(req.body.toString());
const deliveryId = req.headers["hellojohn-delivery-id"] as string;
// Check if we've already processed this delivery
const alreadyProcessed = await db.webhook_events.exists({ delivery_id: deliveryId });
if (alreadyProcessed) {
return res.sendStatus(200); // Acknowledge without re-processing
}
// Record the delivery before processing
await db.webhook_events.insert({ delivery_id: deliveryId, event_type: event.type });
// Process the event
await handleEvent(event);
res.sendStatus(200);
});Respond Quickly
HelloJohn expects a response within 30 seconds. If your processing takes longer, respond immediately and process asynchronously:
app.post("/webhooks/hellojohn", async (req, res) => {
const event = JSON.parse(req.body.toString());
// Respond immediately
res.sendStatus(200);
// Process async
setImmediate(async () => {
try {
await processEvent(event);
} catch (err) {
console.error("Webhook processing failed:", err);
// Log to your error tracker
}
});
});Or use a job queue (BullMQ, SQS, etc.):
app.post("/webhooks/hellojohn", async (req, res) => {
const event = JSON.parse(req.body.toString());
// Enqueue for background processing
await webhookQueue.add("process", { event });
res.sendStatus(200);
});Delivery Logs
View delivery history for a webhook:
curl "https://api.hellojohn.dev/v1/webhooks/wh_01HABCDEF777001/deliveries?status=failed" \
-H "Authorization: Bearer sk_live_abc123" \
-H "X-Tenant-ID: tnt_01HABCDEF654321"Response:
{
"data": [
{
"id": "del_01HABCDEF888002",
"event": "user.created",
"status": "failed",
"http_status": 500,
"attempts": 3,
"next_retry_at": "2024-01-20T10:30:00Z",
"created_at": "2024-01-20T08:15:00Z"
}
]
}Manual Retry
Retry a failed delivery:
curl -X POST "https://api.hellojohn.dev/v1/webhooks/deliveries/del_01HABCDEF888002/retry" \
-H "Authorization: Bearer sk_live_abc123" \
-H "X-Tenant-ID: tnt_01HABCDEF654321"Delivery Guarantees
HelloJohn provides at-least-once delivery — every event will be delivered at least once, but may be delivered more than once. Design your handlers to be idempotent.
HelloJohn does not guarantee event ordering. Handle out-of-order events using the created_at timestamp in the payload.