# How to Implement Webhooks with HMAC-Signed Delivery and Exponential Backoff in OmniRoute

> Learn to implement webhooks with HMAC signed delivery and exponential backoff in OmniRoute. This guide details automatic signing and retry strategies for reliable webhook integration.

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

---

**OmniRoute automatically signs webhook payloads with HMAC-SHA256 and retries failed deliveries using exponential backoff, configurable via the dispatcher in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts).**

The **OmniRoute** repository (`diegosouzapw/OmniRoute`) ships with a production-ready webhook subsystem that handles cryptographic signing and resilient delivery out of the box. By configuring a secret per webhook, you enable tamper-proof payload verification, while the built-in retry mechanism ensures temporary network failures do not result in lost events.

## Core Architecture of the Webhook Dispatcher

All webhook logic resides in **[`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts)**. The dispatcher supports four integration types—`custom`, `slack`, `discord`, and `telegram`—with only the `custom` type utilizing HMAC signing. The generic flow uses three primary components:

### HMAC-SHA256 Payload Signing

The **`signPayload`** function (lines 20–22) generates a SHA-256 HMAC from the raw JSON body and the webhook-specific secret. When dispatching a `custom` webhook, the system constructs headers including `X-Webhook-Signature`:

```typescript
const body = JSON.stringify(payload);
const headers: Record<string, string> = {
  "Content-Type": "application/json",
  "User-Agent": "OmniRoute-Webhook/1.0",
  "X-Webhook-Event": payload.event,
  "X-Webhook-Timestamp": payload.timestamp,
};

if (secret) {
  headers["X-Webhook-Signature"] = signPayload(body, secret);
}

```

The signature allows receivers to verify payload integrity using the shared secret.

### Exponential Backoff and Retry Logic

The **`deliverWebhook`** function (lines 71–129) implements the retry loop. It attempts delivery up to `maxRetries` (default 3) using the formula **`2^n × 1s`** where *n* is the attempt index (0, 1, 2). The retry logic triggers on HTTP status codes ≥ 500 or network errors:

```typescript
for (let attempt = 0; attempt <= maxRetries; attempt++) {
  try {
    const res = await fetch(url, { method: "POST", headers, body, signal });
    if (res.ok || res.status < 500) return { success: res.ok, status: res.status };
    if (attempt < maxRetries) await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
  } catch (error) {
    if (attempt === maxRetries) return { success: false, status: 0, error: error.message };
    await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
  }
}

```

Each request also respects a **10-second abort timeout** to prevent hanging connections.

## Configuring Webhooks for HMAC Signing

To implement signed delivery, you must create a webhook record with the appropriate schema defined in **[`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts)**:

1. **Set the `kind` field** to `"custom"` (Slack, Discord, and Telegram bypass HMAC and use integration-specific formatters).
2. **Provide a `secret`** string stored in the `secret` column; OmniRoute uses this to compute the `X-Webhook-Signature` header.
3. **Define `events`** as a JSON array (e.g., `["model.completed", "quota.exhausted"]`) or `"*"` for all events.
4. **Ensure `enabled`** is set to `true` (the default upon creation).

The system automatically disables webhooks after **10 consecutive failures** recorded in **[`src/lib/db/webhookDeliveries.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhookDeliveries.ts)**, protecting against endless retry loops.

## Sending Events

OmniRoute offers two entry points for triggering webhook deliveries depending on your blocking requirements.

### Fire-and-Forget Delivery with notifyWebhookEvent

For hot paths where you cannot await I/O, use the **`notifyWebhookEvent`** function (lines 137–144). This non-blocking API builds the `WebhookPayload` object and schedules delivery asynchronously:

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

// Trigger after a model completes
notifyWebhookEvent("model.completed", {
  modelId: "gpt-4",
  requestId: "abc123",
  durationMs: 245,
});

```

The dispatcher retrieves all enabled webhooks listening to `"model.completed"` (or `"*"`), signs the payload for `custom` types, and handles retries without blocking your application thread.

### Synchronous Delivery with dispatchEvent

For CLI tools or batch processes where you need confirmation, await the **`dispatchEvent`** function (lines 51–221):

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

const results = await dispatchEvent("quota.exhausted", { 
  accountId: "acct42", 
  used: 999, 
  limit: 1000 
});

```

This returns the aggregated results of all delivery attempts, including status codes and error messages.

## Automatic Failure Protection

After each dispatch batch, OmniRoute calls `disableWebhooksWithHighFailures(10)`. Any webhook accumulating 10 or more failures in the `webhookDeliveries` table is automatically disabled. This circuit-breaker pattern prevents the system from repeatedly attempting to reach broken endpoints.

## Summary

- **HMAC signing** is implemented in `signPayload` within [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts) and applies only to `custom` webhook types.
- **Exponential backoff** retries failures up to 3 times using delays of 1s, 2s, and 4s before giving up.
- **Fire-and-forget** usage: call `notifyWebhookEvent` for non-blocking event dispatch.
- **Synchronous usage**: await `dispatchEvent` when you need delivery confirmation.
- **Automatic disabling**: Webhooks with 10 failures are automatically muted to prevent resource waste.

## Frequently Asked Questions

### How does OmniRoute sign webhook payloads?

OmniRoute computes an HMAC-SHA256 hex digest of the JSON-serialized payload body using the secret stored in the webhook's `secret` column. The resulting signature is sent in the `X-Webhook-Signature` header, allowing receivers to verify authenticity by recomputing the digest with the shared secret.

### What is the default retry behavior for failed deliveries?

The dispatcher in `deliverWebhook` retries failed requests up to 3 times by default. It waits `2^n` seconds between attempts (where *n* starts at 0), creating delays of 1 second, 2 seconds, and 4 seconds. Retries occur only on HTTP 5xx errors or network failures; 4xx errors are considered permanent and do not trigger retries.

### How do I integrate Slack, Discord, or Telegram webhooks?

Set the webhook `kind` to `"slack"`, `"discord"`, or `"telegram"` instead of `"custom"`. These integrations bypass HMAC signing and use specialized payload builders located in [`src/lib/webhooks/integrations/slack.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/slack.ts), [`discord.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/discord.ts), and [`telegram.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/telegram.ts). Note that Telegram webhooks decrypt the bot token from `metadata_encrypted` rather than using the standard `secret` field for signing.

### When does OmniRoute disable a webhook automatically?

The system disables a webhook after it accumulates **10 delivery failures**, as tracked in the `webhookDeliveries` table. This check runs after every batch dispatch via `disableWebhooksWithHighFailures(10)`, serving as a circuit breaker to prevent infinite retry loops against broken or unresponsive endpoints.