# How to Configure Webhooks with HMAC Signing and Exponential Backoff in OmniRoute

> Learn how to configure webhooks with HMAC signing & exponential backoff in OmniRoute. Securely send payloads and ensure reliable delivery with automatic retries.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-07-26

---

**OmniRoute provides a built-in webhook system that automatically signs custom payloads using HMAC-SHA256 and retries failed deliveries with exponential backoff, automatically disabling endpoints that exceed a configurable failure threshold.**

Configuring webhooks with HMAC signing and exponential backoff in OmniRoute involves setting up webhook records in SQLite, leveraging automatic payload signing for custom endpoints, and relying on the dispatcher's resilient delivery mechanism. The implementation spans database utilities in [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts) and dispatch logic in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts), offering a complete pipeline for secure, reliable event delivery.

## Webhook Configuration and Storage

OmniRoute persists webhook definitions in a SQLite database through the CRUD utilities in [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts). Each record stores the target URL, subscribed events, an optional secret for signing, the webhook kind (`slack`, `discord`, `telegram`, or `custom`), and metadata including failure counters.

The **failure tracking system** monitors delivery attempts through `recordWebhookDelivery` and automatically disables flaky endpoints via `disableWebhooksWithHighFailures()`. By default, any webhook exceeding **10 failures** is disabled to prevent noise and resource waste.

### Creating a Custom Webhook

To enable HMAC signing, you must create a webhook with `kind: "custom"` and provide a secret (or let OmniRoute generate one automatically):

```typescript
import { createWebhook } from "@/src/lib/db/webhooks";

const myWebhook = createWebhook({
  url: "https://example.com/omniroute-hook",
  events: ["order.completed", "user.signup"],
  secret: "whsec_mySuperSecret",
  kind: "custom",
  description: "Customer-facing order webhook",
});

console.log("Webhook ID:", myWebhook.id);

```

When calling `createWebhook`, the function stores the configuration in SQLite and returns the created record, making it immediately available for event dispatching.

## HMAC Payload Signing for Custom Endpoints

OmniRoute distinguishes between native integrations (Slack, Discord, Telegram) and custom webhooks. Only **custom** webhooks receive HMAC-SHA256 signatures to verify payload authenticity.

In [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts), the `signPayload` function (lines 20-22) generates the signature:

```typescript
// Source: src/lib/webhookDispatcher.ts
const signature = crypto
  .createHmac("sha256", secret)
  .update(JSON.stringify(payload))
  .digest("hex");

return `sha256=${signature}`;

```

The dispatcher attaches this value to the `X-Webhook-Signature` header when delivering custom webhooks. Receiving servers should validate this header against a regenerated HMAC using their stored secret.

## Resilient Delivery with Exponential Backoff

The `deliverWebhook` function (lines 71-88 and 94-124 in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts)) implements a fault-tolerant delivery mechanism with the following characteristics:

- **Retry conditions**: HTTP 5xx responses or network errors trigger retries
- **Maximum attempts**: 3 attempts by default (configurable via the `maxRetries` parameter)
- **Backoff calculation**: `Math.pow(2, attempt) * 1000` milliseconds (1s, 2s, 4s)
- **Failure persistence**: Each attempt is recorded via `recordWebhookDelivery` for observability

The retry loop implementation:

```typescript
// Excerpt from src/lib/webhookDispatcher.ts lines 94-124
for (let attempt = 0; attempt < maxRetries; attempt++) {
  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Webhook-Signature": signature,
      },
      body: JSON.stringify(payload),
    });

    if (response.ok) return { success: true };
    
    // Retry on server errors (5xx)
    if (response.status >= 500) {
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
      continue;
    }
    
    return { success: false, status: response.status };
  } catch (error) {
    // Network errors trigger retry with backoff
    if (attempt < maxRetries - 1) {
      await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
    }
  }
}

```

The `dispatchEvent` function (lines 51-76) orchestrates this process by iterating over enabled webhooks returned from `getEnabledWebhooks()`, building appropriate payloads (raw JSON for Slack/Discord/Telegram, signed for custom), and invoking `deliverWebhook` for each target. After processing all webhooks, it calls `disableWebhooksWithHighFailures(10)` to deactivate any endpoints that have exceeded the failure threshold.

## Practical Implementation Examples

### Emitting Events to Webhooks

Use the fire-and-forget `notifyWebhookEvent` helper to trigger webhooks without awaiting delivery:

```typescript
import { notifyWebhookEvent } from "@/src/lib/webhookDispatcher";

function onOrderCompleted(order) {
  notifyWebhookEvent("order.completed", {
    orderId: order.id,
    amount: order.total,
    customer: order.customerEmail,
  });
}

```

### Manual Delivery with Custom Retry Count

For scenarios requiring explicit control, use `deliverWebhook` directly:

```typescript
import { deliverWebhook } from "@/src/lib/webhookDispatcher";

const payload = {
  event: "user.signup",
  timestamp: new Date().toISOString(),
  data: { userId: "12345", email: "alice@example.com" },
};

await deliverWebhook(
  "https://example.com/omniroute-hook",
  payload,
  "whsec_mySuperSecret",
  5 // maxRetries override
);

```

## Summary

- **Storage layer**: [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts) manages webhook CRUD, failure counting, and automatic disabling via `createWebhook()`, `getEnabledWebhooks()`, and `disableWebhooksWithHighFailures()`.
- **HMAC signing**: Only `kind: "custom"` webhooks receive signatures via `signPayload()` in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts), using SHA-256 with the `sha256=` prefix in the `X-Webhook-Signature` header.
- **Resilient delivery**: `deliverWebhook()` retries up to 3 times (configurable) with exponential backoff (1s, 2s, 4s) for 5xx or network errors, persisting outcomes to the database.
- **Automatic cleanup**: Webhooks exceeding the default failure threshold of 10 are automatically disabled to maintain system health.

## Frequently Asked Questions

### What webhook kinds support HMAC signing in OmniRoute?

Only webhooks with `kind: "custom"` receive HMAC signatures. Native integrations for Slack, Discord, and Telegram bypass the signing process in `src/lib/webhooks/integrations/` and instead use raw JSON payloads optimized for each platform's API requirements.

### How does OmniRoute calculate the retry delay for failed webhooks?

The retry delay follows an exponential backoff formula of **2^n × 1000 milliseconds**, where *n* is the current attempt number (0-indexed). This produces delays of 1 second, 2 seconds, and 4 seconds for the three default retry attempts, as implemented in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts) lines 94-124.

### At what point does OmniRoute automatically disable a webhook?

OmniRoute disables a webhook after it accumulates **10 failed delivery attempts** (configurable). The `disableWebhooksWithHighFailures(10)` function in [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts) queries the failure count stored in the webhook metadata and updates the status to disabled when the threshold is exceeded.

### How can I verify the HMAC signature on the receiving end?

To verify the `X-Webhook-Signature` header, compute an HMAC-SHA256 digest of the raw request body using your stored secret, prefix the result with `sha256=`, and perform a constant-time comparison against the received header value. The signing implementation in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts) uses `crypto.createHmac("sha256", secret)` on the JSON-stringified payload.