HelloJohn / docs
Webhooks

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:

  1. HelloJohn serializes the event payload
  2. Makes a POST request to your endpoint within 1 second
  3. Waits up to 30 seconds for a response
  4. If 2xx response received → delivery marked succeeded
  5. 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
HeaderDescription
HelloJohn-SignatureHMAC-SHA256 signature for verification
HelloJohn-Delivery-IDUnique ID for this delivery attempt
HelloJohn-EventThe event type
HelloJohn-TimestampUnix timestamp when the event was created

Retry Schedule

If delivery fails, HelloJohn retries with exponential backoff:

AttemptDelay
1 (initial)Immediate
25 minutes
330 minutes
42 hours
55 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.


On this page