Retry Policy

When a webhook delivery fails, Otesse automatically retries with exponential backoff. Here is the full retry behavior.

What Counts as a Failure

A delivery is considered failed if:

  • Your endpoint does not respond within 30 seconds (timeout)
  • Your endpoint returns a non-2xx HTTP status code (3xx, 4xx, 5xx)
  • The connection cannot be established (DNS failure, connection refused, SSL error)

Retry Schedule

Failed deliveries are retried with increasing delays:

AttemptDelay After Previous AttemptTotal Time Since First Attempt
1 (original)0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
52 hours~2.5 hours
66 hours~8.5 hours
712 hours~20.5 hours
8 (final)24 hours~44.5 hours

After 8 total attempts (including the original), the delivery is marked as permanently failed.

Monitoring Failures

Dashboard

View delivery status in Settings > Integrations > Webhooks > [Endpoint] > Deliveries:

  • Green = successful delivery (2xx response)
  • Yellow = pending retry
  • Red = permanently failed

Failure Alerts

If your endpoint fails repeatedly:

  • 5 consecutive failures — Warning email sent to admin
  • 100 consecutive failures — Webhook endpoint is automatically disabled
  • Disabled endpoint — No more deliveries are attempted until you re-enable it

Handling Failures in Your Application

Return 200 Quickly

Process events asynchronously. Accept the webhook, return 200, and process the event in a background job:

app.post('/webhooks/otesse', async (req, res) => {
  // Verify signature
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid');
  }

  // Acknowledge receipt immediately
  res.status(200).send('OK');

  // Process asynchronously
  await queue.add('processWebhook', {
    event: req.body,
    deliveryId: req.headers['x-otesse-delivery'],
  });
});

Implement Idempotency

Since retries can deliver the same event multiple times:

async function processWebhook(event, deliveryId) {
  // Check if already processed
  const existing = await db.webhookDeliveries.findOne({ deliveryId });
  if (existing) {
    console.log('Duplicate delivery, skipping:', deliveryId);
    return;
  }

  // Process the event
  await handleEvent(event);

  // Record the delivery
  await db.webhookDeliveries.insert({
    deliveryId,
    eventType: event.type,
    processedAt: new Date(),
  });
}

Handle Out-of-Order Delivery

Events may arrive out of order during retries. Use the created_at timestamp to determine event sequence:

async function handleBookingUpdate(event) {
  const booking = await db.bookings.findOne({ id: event.data.id });

  // Only apply if this event is newer than our current data
  if (booking && new Date(event.created_at) <= new Date(booking.last_webhook_at)) {
    console.log('Stale event, skipping');
    return;
  }

  await db.bookings.update({ id: event.data.id }, {
    ...event.data,
    last_webhook_at: event.created_at,
  });
}

Replaying Events

If you need to reprocess events (e.g., after fixing a bug in your handler):

  1. Go to the webhook delivery history
  2. Filter by date range and event type
  3. Click Replay on individual deliveries or Replay All for a filtered set
  4. Events are re-sent with the same payload but a new delivery ID