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:
- Distribu looks up every active webhook for this company that's subscribed to this event type.
- 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.
- 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 (
3xxredirects,4xx,5xx). - Connection refused / DNS failure.
- TLS errors.
- Timeout (no response after 10s).
- Any exception during the fetch.
- Non-2xx HTTP response (
statusCode is populated for HTTP responses, null for network-level
failures.
The delivery log
Every delivery attempt is logged with:
| Field | Description |
|---|---|
event | Event name, e.g. order.created |
payload | The full JSON envelope sent |
statusCode | HTTP status or empty for network errors |
responseBody | Body from your endpoint, truncated to 2000 chars |
durationMs | How long the request took |
success | true if 2xx, false otherwise |
createdAt | When 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:
- Event —
order.createdetc. - Status — green pill with the HTTP code (e.g.
200), or a red pill showingERRfor 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 viaPATCH). Distribu will fire a fresh webhook with the new transition.order.createdcan 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.
