How to Configure Webhooks with HMAC Signing and Exponential Backoff in OmniRoute
OmniRoute provides a built-in webhook system that automatically signs custom payloads using HMAC-SHA256 and retries failed deliveries with exponential backoff, automatically disabling endpoints that exceed a configurable failure threshold.
Configuring webhooks with HMAC signing and exponential backoff in OmniRoute involves setting up webhook records in SQLite, leveraging automatic payload signing for custom endpoints, and relying on the dispatcher's resilient delivery mechanism. The implementation spans database utilities in src/lib/db/webhooks.ts and dispatch logic in src/lib/webhookDispatcher.ts, offering a complete pipeline for secure, reliable event delivery.
Webhook Configuration and Storage
OmniRoute persists webhook definitions in a SQLite database through the CRUD utilities in src/lib/db/webhooks.ts. Each record stores the target URL, subscribed events, an optional secret for signing, the webhook kind (slack, discord, telegram, or custom), and metadata including failure counters.
The failure tracking system monitors delivery attempts through recordWebhookDelivery and automatically disables flaky endpoints via disableWebhooksWithHighFailures(). By default, any webhook exceeding 10 failures is disabled to prevent noise and resource waste.
Creating a Custom Webhook
To enable HMAC signing, you must create a webhook with kind: "custom" and provide a secret (or let OmniRoute generate one automatically):
import { createWebhook } from "@/src/lib/db/webhooks";
const myWebhook = createWebhook({
url: "https://example.com/omniroute-hook",
events: ["order.completed", "user.signup"],
secret: "whsec_mySuperSecret",
kind: "custom",
description: "Customer-facing order webhook",
});
console.log("Webhook ID:", myWebhook.id);
When calling createWebhook, the function stores the configuration in SQLite and returns the created record, making it immediately available for event dispatching.
HMAC Payload Signing for Custom Endpoints
OmniRoute distinguishes between native integrations (Slack, Discord, Telegram) and custom webhooks. Only custom webhooks receive HMAC-SHA256 signatures to verify payload authenticity.
In src/lib/webhookDispatcher.ts, the signPayload function (lines 20-22) generates the signature:
// Source: src/lib/webhookDispatcher.ts
const signature = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");
return `sha256=${signature}`;
The dispatcher attaches this value to the X-Webhook-Signature header when delivering custom webhooks. Receiving servers should validate this header against a regenerated HMAC using their stored secret.
Resilient Delivery with Exponential Backoff
The deliverWebhook function (lines 71-88 and 94-124 in src/lib/webhookDispatcher.ts) implements a fault-tolerant delivery mechanism with the following characteristics:
- Retry conditions: HTTP 5xx responses or network errors trigger retries
- Maximum attempts: 3 attempts by default (configurable via the
maxRetriesparameter) - Backoff calculation:
Math.pow(2, attempt) * 1000milliseconds (1s, 2s, 4s) - Failure persistence: Each attempt is recorded via
recordWebhookDeliveryfor observability
The retry loop implementation:
// Excerpt from src/lib/webhookDispatcher.ts lines 94-124
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
},
body: JSON.stringify(payload),
});
if (response.ok) return { success: true };
// Retry on server errors (5xx)
if (response.status >= 500) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
continue;
}
return { success: false, status: response.status };
} catch (error) {
// Network errors trigger retry with backoff
if (attempt < maxRetries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
The dispatchEvent function (lines 51-76) orchestrates this process by iterating over enabled webhooks returned from getEnabledWebhooks(), building appropriate payloads (raw JSON for Slack/Discord/Telegram, signed for custom), and invoking deliverWebhook for each target. After processing all webhooks, it calls disableWebhooksWithHighFailures(10) to deactivate any endpoints that have exceeded the failure threshold.
Practical Implementation Examples
Emitting Events to Webhooks
Use the fire-and-forget notifyWebhookEvent helper to trigger webhooks without awaiting delivery:
import { notifyWebhookEvent } from "@/src/lib/webhookDispatcher";
function onOrderCompleted(order) {
notifyWebhookEvent("order.completed", {
orderId: order.id,
amount: order.total,
customer: order.customerEmail,
});
}
Manual Delivery with Custom Retry Count
For scenarios requiring explicit control, use deliverWebhook directly:
import { deliverWebhook } from "@/src/lib/webhookDispatcher";
const payload = {
event: "user.signup",
timestamp: new Date().toISOString(),
data: { userId: "12345", email: "alice@example.com" },
};
await deliverWebhook(
"https://example.com/omniroute-hook",
payload,
"whsec_mySuperSecret",
5 // maxRetries override
);
Summary
- Storage layer:
src/lib/db/webhooks.tsmanages webhook CRUD, failure counting, and automatic disabling viacreateWebhook(),getEnabledWebhooks(), anddisableWebhooksWithHighFailures(). - HMAC signing: Only
kind: "custom"webhooks receive signatures viasignPayload()insrc/lib/webhookDispatcher.ts, using SHA-256 with thesha256=prefix in theX-Webhook-Signatureheader. - Resilient delivery:
deliverWebhook()retries up to 3 times (configurable) with exponential backoff (1s, 2s, 4s) for 5xx or network errors, persisting outcomes to the database. - Automatic cleanup: Webhooks exceeding the default failure threshold of 10 are automatically disabled to maintain system health.
Frequently Asked Questions
What webhook kinds support HMAC signing in OmniRoute?
Only webhooks with kind: "custom" receive HMAC signatures. Native integrations for Slack, Discord, and Telegram bypass the signing process in src/lib/webhooks/integrations/ and instead use raw JSON payloads optimized for each platform's API requirements.
How does OmniRoute calculate the retry delay for failed webhooks?
The retry delay follows an exponential backoff formula of 2^n × 1000 milliseconds, where n is the current attempt number (0-indexed). This produces delays of 1 second, 2 seconds, and 4 seconds for the three default retry attempts, as implemented in src/lib/webhookDispatcher.ts lines 94-124.
At what point does OmniRoute automatically disable a webhook?
OmniRoute disables a webhook after it accumulates 10 failed delivery attempts (configurable). The disableWebhooksWithHighFailures(10) function in src/lib/db/webhooks.ts queries the failure count stored in the webhook metadata and updates the status to disabled when the threshold is exceeded.
How can I verify the HMAC signature on the receiving end?
To verify the X-Webhook-Signature header, compute an HMAC-SHA256 digest of the raw request body using your stored secret, prefix the result with sha256=, and perform a constant-time comparison against the received header value. The signing implementation in src/lib/webhookDispatcher.ts uses crypto.createHmac("sha256", secret) on the JSON-stringified payload.
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 →