How to Implement Webhooks with HMAC-Signed Delivery and Exponential Backoff in OmniRoute
OmniRoute automatically signs webhook payloads with HMAC-SHA256 and retries failed deliveries using exponential backoff, configurable via the dispatcher in src/lib/webhookDispatcher.ts.
The OmniRoute repository (diegosouzapw/OmniRoute) ships with a production-ready webhook subsystem that handles cryptographic signing and resilient delivery out of the box. By configuring a secret per webhook, you enable tamper-proof payload verification, while the built-in retry mechanism ensures temporary network failures do not result in lost events.
Core Architecture of the Webhook Dispatcher
All webhook logic resides in src/lib/webhookDispatcher.ts. The dispatcher supports four integration types—custom, slack, discord, and telegram—with only the custom type utilizing HMAC signing. The generic flow uses three primary components:
HMAC-SHA256 Payload Signing
The signPayload function (lines 20–22) generates a SHA-256 HMAC from the raw JSON body and the webhook-specific secret. When dispatching a custom webhook, the system constructs headers including X-Webhook-Signature:
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 signature allows receivers to verify payload integrity using the shared secret.
Exponential Backoff and Retry Logic
The deliverWebhook function (lines 71–129) implements the retry loop. It attempts delivery up to maxRetries (default 3) using the formula 2^n × 1s where n is the attempt index (0, 1, 2). The retry logic triggers on HTTP status codes ≥ 500 or network errors:
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 };
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.
Configuring Webhooks for HMAC Signing
To implement signed delivery, you must create a webhook record with the appropriate schema defined in src/lib/db/webhooks.ts:
- Set the
kindfield to"custom"(Slack, Discord, and Telegram bypass HMAC and use integration-specific formatters). - Provide a
secretstring stored in thesecretcolumn; OmniRoute uses this to compute theX-Webhook-Signatureheader. - Define
eventsas a JSON array (e.g.,["model.completed", "quota.exhausted"]) or"*"for all events. - Ensure
enabledis set totrue(the default upon creation).
The system automatically disables webhooks after 10 consecutive failures recorded in src/lib/db/webhookDeliveries.ts, protecting against endless retry loops.
Sending Events
OmniRoute offers two entry points for triggering webhook deliveries depending on your blocking requirements.
Fire-and-Forget Delivery with notifyWebhookEvent
For hot paths where you cannot await I/O, use the notifyWebhookEvent function (lines 137–144). This non-blocking API builds the WebhookPayload object and schedules delivery asynchronously:
import { notifyWebhookEvent } from "@/lib/webhookDispatcher";
// Trigger after a model completes
notifyWebhookEvent("model.completed", {
modelId: "gpt-4",
requestId: "abc123",
durationMs: 245,
});
The dispatcher retrieves all enabled webhooks listening to "model.completed" (or "*"), signs the payload for custom types, and handles retries without blocking your application thread.
Synchronous Delivery with dispatchEvent
For CLI tools or batch processes where you need confirmation, await the dispatchEvent function (lines 51–221):
import { dispatchEvent } from "@/lib/webhookDispatcher";
const results = await dispatchEvent("quota.exhausted", {
accountId: "acct42",
used: 999,
limit: 1000
});
This returns the aggregated results of all delivery attempts, including status codes and error messages.
Automatic Failure Protection
After each dispatch batch, OmniRoute calls disableWebhooksWithHighFailures(10). Any webhook accumulating 10 or more failures in the webhookDeliveries table is automatically disabled. This circuit-breaker pattern prevents the system from repeatedly attempting to reach broken endpoints.
Summary
- HMAC signing is implemented in
signPayloadwithinsrc/lib/webhookDispatcher.tsand applies only tocustomwebhook types. - Exponential backoff retries failures up to 3 times using delays of 1s, 2s, and 4s before giving up.
- Fire-and-forget usage: call
notifyWebhookEventfor non-blocking event dispatch. - Synchronous usage: await
dispatchEventwhen you need delivery confirmation. - Automatic disabling: Webhooks with 10 failures are automatically muted to prevent resource waste.
Frequently Asked Questions
How does OmniRoute sign webhook payloads?
OmniRoute computes an HMAC-SHA256 hex digest of the JSON-serialized payload body using the secret stored in the webhook's secret column. The resulting signature is sent in the X-Webhook-Signature header, allowing receivers to verify authenticity by recomputing the digest with the shared secret.
What is the default retry behavior for failed deliveries?
The dispatcher in deliverWebhook retries failed requests up to 3 times by default. It waits 2^n seconds between attempts (where n starts at 0), creating delays of 1 second, 2 seconds, and 4 seconds. Retries occur only on HTTP 5xx errors or network failures; 4xx errors are considered permanent and do not trigger retries.
How do I integrate Slack, Discord, or Telegram webhooks?
Set the webhook kind to "slack", "discord", or "telegram" instead of "custom". These integrations bypass HMAC signing and use specialized payload builders located in src/lib/webhooks/integrations/slack.ts, discord.ts, and telegram.ts. Note that Telegram webhooks decrypt the bot token from metadata_encrypted rather than using the standard secret field for signing.
When does OmniRoute disable a webhook automatically?
The system disables a webhook after it accumulates 10 delivery failures, as tracked in the webhookDeliveries table. This check runs after every batch dispatch via disableWebhooksWithHighFailures(10), serving as a circuit breaker to prevent infinite retry loops against broken or unresponsive endpoints.
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 →