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/jsonUser-Agent: OmniRoute-Webhook/1.0X-Webhook-Event: The event type (e.g.,model.completed)X-Webhook-Timestamp: ISO timestamp of the payloadX-Webhook-Signature: HMAC-SHA256 hex digest (only whensecretis 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, ortelegram)
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:
- custom: Full HMAC signing and exponential backoff (lines 71-129 in
src/lib/webhookDispatcher.ts) - slack: Uses
deliverRaw(lines 39-68) with Slack-specific payload formatting fromsrc/lib/webhooks/integrations/slack.ts - discord: Bypasses HMAC, uses Discord payload builder in
src/lib/webhooks/integrations/discord.ts - telegram: Bypasses HMAC, decrypts bot token from
metadata_encryptedfield usingsrc/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
customwebhooks, invokesdeliverWebhookwith 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
secretin the webhook record; OmniRoute usessignPayloadto generateX-Webhook-Signatureheaders insrc/lib/webhookDispatcher.ts. - Exponential backoff follows a
2^nsecond delay between retries, with a default maximum of 3 attempts configurable viamaxRetries. - Integration types: Choose
customfor signed payloads; useslack,discord, ortelegramfor specific platforms that bypass signing. - API options: Use
notifyWebhookEventfor fire-and-forget delivery in production code, ordispatchEventfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →