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

> Learn to set up webhooks with HMAC signing and exponential backoff in OmniRoute. Secure your integrations and ensure reliable deliveries with automated retries.

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

---

**Configure a webhook record with a `secret` for HMAC signing, select the `custom` integration type, and invoke `notifyWebhookEvent()` to automatically trigger signed deliveries with exponential backoff retries.**

OmniRoute provides a built-in webhook subsystem that delivers events to HTTP endpoints with enterprise-grade security and reliability. The dispatcher, implemented in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts), handles **HMAC-SHA256 payload signing**, **exponential backoff retries**, and automatic failure detection out of the box.

## How OmniRoute Handles Webhook Security and Reliability

The webhook architecture centers on three core guarantees: cryptographic payload verification, resilient delivery with backoff, and automatic disablement of unstable endpoints. All logic resides in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts), with database abstractions in [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts) and delivery logging in [`src/lib/db/webhookDeliveries.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhookDeliveries.ts).

### HMAC-SHA256 Payload Signing

When you configure a webhook with a `secret`, OmniRoute automatically signs every outbound request using the `signPayload` function (lines 20-22 in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts)). The implementation generates a SHA-256 HMAC from the raw JSON body and your stored secret, injecting the result into the `X-Webhook-Signature` header.

The complete header set for signed deliveries includes:
- `Content-Type: application/json`
- `User-Agent: OmniRoute-Webhook/1.0`
- `X-Webhook-Event`: The event type (e.g., `model.completed`)
- `X-Webhook-Timestamp`: ISO timestamp of the payload
- `X-Webhook-Signature`: HMAC-SHA256 hex digest (only when `secret` is present)

```typescript
// Excerpt from deliverWebhook (lines 71-129)
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);
}

```

### Exponential Backoff Retry Logic

The `deliverWebhook` function implements a retry loop that waits `2^n × 1 second` between attempts, where `n` is the current attempt index. By default, OmniRoute retries up to 3 times, but this is configurable per call via the `maxRetries` parameter.

The retry strategy activates on HTTP status codes ≥ 500 or network-level fetch errors. After exhausting all retries, the function returns `{ success: false, status: 0, error: … }`, which triggers logging in the `webhookDeliveries` table.

```typescript
// Simplified retry loop from deliverWebhook
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 };
    // Server error triggers backoff
    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));
  }
}

```

## Configuring Your Webhook in OmniRoute

### Database Schema and Required Fields

Create a webhook record by inserting into the database schema defined in [`src/lib/db/webhooks.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts). The following fields are mandatory:

- **url**: The HTTPS endpoint to receive events
- **events**: Array of event strings (e.g., `["model.completed", "quota.exhausted"]`) or `["*"]` for all events
- **kind**: Integration type (`custom`, `slack`, `discord`, or `telegram`)

Optional but recommended for security:
- **secret**: Random string used as the HMAC key
- **enabled**: Boolean flag (defaults to true on creation)

```sql
-- Example insertion (conceptual)
INSERT INTO webhooks (url, events, kind, secret, enabled)
VALUES ('https://api.example.com/webhooks/omniroute', '["*"]', 'custom', 'whsec_...', true);

```

### Choosing Between Integration Types

OmniRoute supports four integration types, but only `custom` triggers the HMAC signing flow:

1. **custom**: Full HMAC signing and exponential backoff (lines 71-129 in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts))
2. **slack**: Uses `deliverRaw` (lines 39-68) with Slack-specific payload formatting from [`src/lib/webhooks/integrations/slack.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/slack.ts)
3. **discord**: Bypasses HMAC, uses Discord payload builder in [`src/lib/webhooks/integrations/discord.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/discord.ts)
4. **telegram**: Bypasses HMAC, decrypts bot token from `metadata_encrypted` field using [`src/lib/webhooks/integrations/telegram.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/telegram.ts)

Use `kind: 'custom'` when you need verifiable signatures and retry guarantees.

## Sending Events with the Dispatcher API

### Fire-and-Forget Delivery

For hot paths where you cannot await network I/O, use `notifyWebhookEvent` (lines 137-144). This function builds the `WebhookPayload` object, retrieves enabled webhooks for the event, and delegates to the appropriate delivery helper without blocking the caller.

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

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

```

Under the hood, `notifyWebhookEvent` calls `dispatchEvent` (lines 51-221), which:
- Looks up webhooks matching the event type or wildcard `*`
- For `custom` webhooks, invokes `deliverWebhook` with HMAC headers
- Applies the 10-second abort timeout and exponential backoff
- Logs outcomes to `webhookDeliveries`

### Synchronous Delivery

For CLI tools, tests, or administrative scripts where you need delivery confirmation, await `dispatchEvent` directly:

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

const result = await dispatchEvent("quota.exhausted", {
  accountId: "acct42",
  used: 999,
  limit: 1000
});
// Returns aggregated results from all webhook deliveries

```

## Automatic Failure Handling

OmniRoute protects your system from endlessly retrying broken endpoints. After each dispatch batch, the system executes `disableWebhooksWithHighFailures(10)`, which queries the `webhookDeliveries` table. Any webhook accumulating 10 or more failures is automatically disabled by setting `enabled = false` in the database.

This circuit-breaker pattern prevents the exponential backoff loop from consuming resources on permanently failed endpoints while preserving logs for forensic analysis.

## Summary

- **HMAC signing** is automatic when you provide a `secret` in the webhook record; OmniRoute uses `signPayload` to generate `X-Webhook-Signature` headers in [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts).
- **Exponential backoff** follows a `2^n` second delay between retries, with a default maximum of 3 attempts configurable via `maxRetries`.
- **Integration types**: Choose `custom` for signed payloads; use `slack`, `discord`, or `telegram` for specific platforms that bypass signing.
- **API options**: Use `notifyWebhookEvent` for fire-and-forget delivery in production code, or `dispatchEvent` for synchronous control in scripts.
- **Failure protection**: Webhooks with 10 failed deliveries are automatically disabled to prevent retry storms.

## Frequently Asked Questions

### What algorithm does OmniRoute use for webhook signing?

OmniRoute uses **HMAC-SHA256** as implemented in the `signPayload` function at lines 20-22 of [`src/lib/webhookDispatcher.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts). The signature is computed over the raw JSON string of the payload and transmitted in the `X-Webhook-Signature` header as a hexadecimal digest.

### How many retries does OmniRoute attempt before giving up?

By default, OmniRoute attempts **3 retries** (4 total attempts including the initial request). This is configurable per call to `deliverWebhook` via the `maxRetries` parameter. The backoff interval follows an exponential pattern: 1 second, 2 seconds, then 4 seconds between attempts.

### Can I disable HMAC signing for specific endpoints?

Yes. HMAC signing is only applied to webhooks with `kind: 'custom'`. If you select `slack`, `discord`, or `telegram` as the integration type, OmniRoute uses the `deliverRaw` helper (lines 39-68) which does not generate the `X-Webhook-Signature` header. Alternatively, omit the `secret` field from a `custom` webhook to disable signing for that specific endpoint.

### Where does OmniRoute store webhook delivery logs?

Delivery attempts are persisted in the `webhookDeliveries` table via the helpers in [`src/lib/db/webhookDeliveries.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhookDeliveries.ts). Each row records the webhook ID, HTTP status code, latency, error message (if any), and timestamp. This table powers the automatic disablement feature that triggers after 10 failures.