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

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, 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, with database abstractions in src/lib/db/webhooks.ts and delivery logging in 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). 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)
// 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.

// 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. 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)
-- 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)
  2. slack: Uses deliverRaw (lines 39-68) with Slack-specific payload formatting from src/lib/webhooks/integrations/slack.ts
  3. discord: Bypasses HMAC, uses Discord payload builder in src/lib/webhooks/integrations/discord.ts
  4. telegram: Bypasses HMAC, decrypts bot token from metadata_encrypted field using 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.

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:

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.
  • 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. 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →