How to Configure OmniRoute Webhooks with HMAC Signing and Exponential Backoff
To configure OmniRoute webhooks with HMAC signing and exponential backoff, set a secret in your webhook record to enable SHA-256 payload signing, then use notifyWebhookEvent or dispatchEvent to trigger deliveries that automatically retry up to 3 times with exponential backoff.
OmniRoute ships with a built-in webhook subsystem located in src/lib/webhookDispatcher.ts that delivers events to HTTP endpoints with cryptographic payload verification and resilient retry logic. Whether you are integrating with internal APIs or external services, understanding how to enable HMAC signing and configure the backoff behavior ensures reliable event delivery.
Core Architecture
The webhook dispatcher in src/lib/webhookDispatcher.ts handles three critical concerns: cryptographic signing, transport resilience, and failure management. When you configure a webhook with a secret, the system uses HMAC-SHA256 to sign every payload, while failed deliveries trigger an exponential backoff sequence.
Payload Signing with HMAC-SHA256
When a webhook record includes a secret value, OmniRoute generates a signature using the signPayload helper (lines 20-22) and attaches it to the X-Webhook-Signature header. The dispatcher constructs the following headers for every signed request:
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 signPayload function computes a SHA-256 HMAC from the raw JSON body and your configured secret. Receiving servers should verify this signature to ensure payload authenticity.
Exponential Backoff Retry Logic
Failed deliveries automatically retry using exponential backoff implemented in the deliverWebhook function (lines 71-129). The logic follows this pattern:
- Retry condition: HTTP status ≥ 500 or network errors
- Delay calculation:
2^n × 1000mswhere n is the current attempt count (0-indexed) - Maximum attempts: Default of 3 retries (configurable via
maxRetries)
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));
}
}
Each request also respects a 10-second abort timeout to prevent hanging connections.
Setting Up Webhooks with HMAC Signing
To enable HMAC signing and exponential backoff for your endpoints, create a webhook record in the database (schema defined in src/lib/db/webhooks.ts) with the following configuration:
- Set the URL: The destination HTTP endpoint
- Choose the kind: Use
"custom"for HMAC-signed payloads (Slack, Discord, and Telegram bypass signing) - Configure the secret: Store a random string in the
secretcolumn to activate HMAC signing - Select events: Specify event types (e.g.,
"model.completed","quota.exhausted") or use"*"for all events - Enable the webhook: Ensure the
enabledflag istrue
The system persists delivery attempts in src/lib/db/webhookDeliveries.ts and automatically disables webhooks after 10 accumulated failures via disableWebhooksWithHighFailures(10).
Sending Events to Configured Webhooks
OmniRoute provides two APIs for dispatching events, both of which respect your HMAC and retry configuration.
Fire-and-Forget Delivery
For hot paths where you cannot await network I/O, use notifyWebhookEvent (lines 137-144):
import { notifyWebhookEvent } from "@/lib/webhookDispatcher";
// Trigger after a model completes
notifyWebhookEvent("model.completed", {
modelId: "gpt-4",
requestId: "abc123",
durationMs: 245,
});
This function returns immediately while the dispatcher asynchronously builds the payload, retrieves enabled webhooks, and calls deliverWebhook for each custom kind webhook with signing and retries.
Awaitable Delivery
For CLI tools, tests, or synchronous flows, use dispatchEvent (lines 51-221) directly:
import { dispatchEvent } from "@/lib/webhookDispatcher";
const result = await dispatchEvent("quota.exhausted", {
accountId: "acct42",
used: 999,
limit: 1000
});
This awaitable method provides granular control and returns the delivery outcome, including any final error states after exhaustion of the exponential backoff retries.
Handling Integration-Specific Formats
While the HMAC signing flow applies to kind: "custom" webhooks, OmniRoute supports platform-specific formats that bypass signing:
- Slack: Uses
src/lib/webhooks/integrations/slack.tsto format payloads - Discord: Uses
src/lib/webhooks/integrations/discord.ts - Telegram: Uses
src/lib/webhooks/integrations/telegram.ts(decrypts bot tokens frommetadata_encrypted)
These integrations use the deliverRaw helper (lines 39-68) which omits HMAC headers but retains the 10-second timeout.
Summary
- HMAC signing is enabled by setting a
secreton webhook records insrc/lib/db/webhooks.ts, which triggers thesignPayloadfunction to addX-Webhook-Signatureheaders. - Exponential backoff retries failed deliveries (HTTP ≥ 500 or network errors) up to 3 times using delays of 1s, 2s, and 4s, implemented in
deliverWebhook. - Dispatch APIs include
notifyWebhookEventfor non-blocking fire-and-forget usage anddispatchEventfor synchronous awaiting. - Automatic protection disables webhooks after 10 delivery failures to prevent resource waste.
- Platform integrations (Slack, Discord, Telegram) use unsigned
deliverRawwhile custom webhooks use signeddeliverWebhook.
Frequently Asked Questions
How do I verify the HMAC signature on my receiving server?
Extract the X-Webhook-Signature header and compute your own HMAC-SHA256 digest using the raw request body and your shared secret. Compare the computed signature with the header value using a timing-safe comparison function. OmniRoute generates this signature in src/lib/webhookDispatcher.ts lines 20-22 using the exact JSON stringification of the payload.
Can I customize the number of retry attempts?
Yes, the deliverWebhook function accepts a maxRetries parameter that defaults to 3. When calling dispatchEvent directly, you can influence retry behavior by modifying the webhook configuration or implementing custom wrapper logic, though the public API exposes the default backoff sequence of 2^n × 1s delays.
Why is my webhook not receiving the X-Webhook-Signature header?
The header is only included for webhooks with kind: "custom" that have a non-empty secret column. If you configured the webhook as slack, discord, or telegram, the system uses deliverRaw (lines 39-68) which does not perform HMAC signing. Additionally, ensure the webhook is enabled and has not been auto-disabled due to 10 consecutive failures.
What happens if the exponential backoff retries all fail?
After exhausting all retry attempts (default 3), the deliverWebhook function returns { success: false, status: 0, error: … } and the failure is recorded in webhookDeliveries. If a webhook accumulates 10 total failures, the system automatically sets enabled: false via disableWebhooksWithHighFailures(10) to prevent further delivery attempts until manually re-enabled.
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 →