Retries & delivery

Every webhook event is fired with the same simple mechanism: one HTTP POST, 10-second timeout, delivery logged regardless of outcome. There's no retry queue, no exponential backoff, no dead-letter inbox today.

If your endpoint is down when the event fires, the event is lost (as far as Distribu's push-delivery is concerned). It's still in the database — you can always reach it via the REST API — but Distribu won't try the webhook again.

This page covers what Distribu does do today, and how to build a resilient integration around the "no retries" constraint.

How delivery works

When an event fires:

  1. Distribu looks up every active webhook for this company that's subscribed to this event type.
  2. For each matching webhook, in parallel:
    • Computes the HMAC-SHA256 signature of the payload.
    • Opens an HTTP POST to the webhook's URL with:
      • X-Webhook-Signature — the signature.
      • X-Webhook-Event — the event name.
      • Content-Type: application/json.
    • Waits up to 10 seconds for the response.
    • On response, reads the body (truncated to 2000 chars) and records a delivery row.
    • On timeout or network error, records a delivery row with no status code and the error message stored as the response body.
  3. That's the end of it. Whether the response was 200 or 500, whether the socket was dropped or the TLS handshake timed out — one attempt only.

The whole thing is fire-and-forget from the business-logic side: creating an order never blocks on webhook delivery, and webhook failures never fail the order.

What counts as a successful delivery

  • success: true — any 2xx response (200, 201, 204, etc.) returned within 10 seconds.
  • success: false — any of:
    • Non-2xx HTTP response (3xx redirects, 4xx, 5xx).
    • Connection refused / DNS failure.
    • TLS errors.
    • Timeout (no response after 10s).
    • Any exception during the fetch.

statusCode is populated for HTTP responses, null for network-level failures.

The delivery log

Every delivery attempt is logged with:

FieldDescription
eventEvent name, e.g. order.created
payloadThe full JSON envelope sent
statusCodeHTTP status or empty for network errors
responseBodyBody from your endpoint, truncated to 2000 chars
durationMsHow long the request took
successtrue if 2xx, false otherwise
createdAtWhen the attempt happened

The dashboard surfaces the most recent 5 deliveries per webhook on Settings → Webhooks. Expand the "Recent deliveries" accordion on each webhook card.

Columns in the UI:

  • Eventorder.created etc.
  • Status — green pill with the HTTP code (e.g. 200), or a red pill showing ERR for network-level failures.
  • Duration — milliseconds, or if we never got a response.
  • Time — local timestamp of the attempt.

There's no full paginated log in the UI today. If you need older deliveries for auditing, email support@distribu.app and we can pull them for you.

Why no automatic retries?

The short version: we haven't built it yet. Webhook retry infrastructure — an exponential backoff schedule, a persistent queue, a dead-letter inbox, a retry-trigger API — is on the roadmap but not shipped.

In the meantime, the rest of this page covers the workarounds.

Building a resilient receiver

1. Make your handler idempotent

Even without retries today, you should code for an eventual retry future — and for the rare possibility of a duplicate delivery. Key off the data.id field (the order's ID) and store whatever "I've handled this" state your system needs.

async function handleOrderCreated(data) {
  const existing = await db.orders.findByExternalId(data.id);
  if (existing) return; // Already processed
  await db.orders.create({ externalId: data.id, ... });
}

2. Respond fast, process async

Distribu's 10-second timeout is generous, but you should still accept the webhook, queue the work, and respond immediately. Long-running handlers are the most common cause of ERR rows in the delivery log.

app.post("/webhooks/distribu", async (req, res) => {
  // Verify signature (fast)…
  await jobQueue.enqueue("process-distribu-event", { body: req.body });
  res.sendStatus(200); // Respond within milliseconds
});

3. Periodic reconciliation

Since retries don't exist, the single most important defense is a periodic sync from the REST API. A scheduled job that runs every few hours and paginates GET /api/v1/orders will catch anything the webhook missed:

// Every 4 hours, pull orders from the last 24h and upsert them locally
cron.schedule("0 */4 * * *", async () => {
  const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
  for await (const order of paginate(`/api/v1/orders?since=${since}`)) {
    await upsertOrder(order);
  }
});

Webhooks handle the happy path (fast, near-real-time). The scheduled sync handles every other case (your server was down, a handler threw, a bug dropped an event on the floor). Together, the combination is roughly as reliable as any retry-with-backoff scheme.

4. Monitor the delivery log

Check the recent-deliveries accordion for your webhook periodically — if you see a run of non-2xx rows you weren't expecting, investigate before the backlog grows. The delivery log only keeps the most recent deliveries visible in the UI; older ones are still in the database but not dashboard-surfaced.

5. Alert on silent failures

A receiver that throws a 500 generates a visible delivery-log entry. A receiver that's offline entirely generates an ERR row. The worst case is a receiver that returns 200 but silently fails to process — no row flagged, no alert. Add instrumentation on your side (error tracking, success-count metrics) so you notice.

Manual re-fires

There's no "resend this delivery" button today. If you need to reprocess a specific event, the workarounds are:

  • GET /api/v1/orders/{id} — pull the current state and process as if the webhook fired.
  • Re-trigger the event — for an order.status_changed, transition the order to a new status in the dashboard (or via PATCH). Distribu will fire a fresh webhook with the new transition. order.created can only be fired by actually creating a new order, so you can't re-trigger that one directly.

Timeouts in detail

The 10-second timeout is measured from socket-connect to last-byte-received. A timeout shows up in the delivery log with no statusCode and an error string (e.g. The user aborted a request.) in place of a response body.

If your handler legitimately needs more than 10 seconds, you must move the slow work into a background job — responding after the timeout still counts as a failure even if your handler eventually finishes.

Ordering guarantees

There are none. Webhooks are fired in parallel across all matching endpoints, with no ordering across different events. An order.status_changed may arrive before the order.created it succeeded.

Defend against out-of-order delivery by:

  • Using GET /api/v1/orders/{id} to fetch the authoritative current state on any event.
  • Treating events as "something happened — go check" triggers, not as authoritative data sources in their own right.

Duplicate deliveries

Rare in practice, but possible. If you ever see two deliveries with the same event and same data.id, treat them as duplicates. The idempotency pattern in "Building a resilient receiver" above handles this case for free.


That's it for webhooks. Next up: CSV Formats — the file format reference for every CSV import and export in Distribu.