How to Configure Webhooks with HMAC Signing and Exponential Backoff in OmniRoute
Store a secret in the webhook record to enable HMAC-SHA256 signing, then call notifyWebhookEvent() to trigger automatic delivery with exponential-backoff retries across three attempts.
OmniRoute ships with a built-in webhook subsystem that signs payloads, retries failures, and protects downstream services from overload. All dispatch logic lives in [src/lib/webhookDispatcher.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts) according to the diegosouzapw/OmniRoute source code.
How Webhook Delivery Works in OmniRoute
The dispatcher supports four webhook types:
custom— generic HTTP endpoints with full HMAC signing supportslack— Slack-compatible payload formatting, no HMACdiscord— Discord-compatible formatting, no HMACtelegram— Telegram Bot API formatting, no HMAC
Only the custom type uses the signed-payload flow described below.
Setting Up HMAC-Signed Webhooks
Step 1: Create the Webhook Record
Insert a row into the webhooks table (schema in [src/lib/db/webhooks.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhooks.ts)):
| Field | Required | Value |
|---|---|---|
url |
Yes | Target HTTPS endpoint |
events |
Yes | Array like ["model.completed"] or ["*"] for all |
kind |
Yes | "custom" (for HMAC signing) |
secret |
No* | Random string for HMAC-SHA256 |
enabled |
Yes | true |
*Required only if you want HMAC signing; omit for unsigned delivery.
-- Example insertion (adapt to your ORM)
INSERT INTO webhooks (url, events, kind, secret, enabled)
VALUES (
'https://api.example.com/omniroute-events',
'["model.completed", "quota.exhausted"]',
'custom',
'whsec_32_random_bytes_of_entropy_here',
true
);
Step 2: Verify Signature on Your Endpoint
OmniRoute sends these headers on every custom webhook delivery:
Content-Type: application/json
User-Agent: OmniRoute-Webhook/1.0
X-Webhook-Event: model.completed
X-Webhook-Timestamp: 2024-01-15T09:30:00.000Z
X-Webhook-Signature: sha256=a3f5...
The X-Webhook-Signature header contains sha256= followed by the hex-encoded HMAC of the raw JSON body. Compute the signature in your handler:
import hmac
import hashlib
def verify_omniroute_signature(payload_bytes: bytes, secret: str, signature_header: str) -> bool:
expected_sig = hmac.new(
secret.encode('utf-8'),
payload_bytes,
hashlib.sha256
).hexdigest()
# Header format: "sha256=<hex>"
received_sig = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected_sig, received_sig)
The signing implementation in OmniRoute (lines 20-22 of [src/lib/webhookDispatcher.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhookDispatcher.ts)):
function signPayload(body: string, secret: string): string {
const sig = crypto.createHmac("sha256", secret).update(body).digest("hex");
return `sha256=${sig}`;
}
Exponential Backoff Retry Behavior
When you configure webhooks with HMAC signing and exponential backoff in OmniRoute, failed deliveries automatically retry with increasing delays.
Retry triggers:
- HTTP status ≥ 500 (server errors)
- Network timeouts or connection failures
Retry schedule (default 3 retries):
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 (final) | 8 seconds |
The retry loop in deliverWebhook (lines 71-129) implements this logic:
// Simplified from src/lib/webhookDispatcher.ts lines 71-129
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, {
method: "POST",
headers,
body,
signal // 10s abort timeout
});
if (res.ok || res.status < 500) {
return { success: res.ok, status: res.status };
}
// Server error → retry with backoff if attempts remain
if (attempt < maxRetries) {
const delayMs = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delayMs));
}
} catch (error) {
if (attempt === maxRetries) {
return { success: false, status: 0, error: error.message };
}
const delayMs = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delayMs));
}
}
Every request includes a 10-second abort signal via AbortController. You can override maxRetries per call when using deliverWebhook directly, though the public API defaults to 3.
Triggering Webhook Events
Fire-and-Forget (Recommended for Hot Paths)
Use notifyWebhookEvent (lines 137-144) to dispatch without blocking:
import { notifyWebhookEvent } from "@/lib/webhookDispatcher";
// After a model completes
notifyWebhookEvent("model.completed", {
modelId: "gpt-4",
requestId: "abc123",
durationMs: 245,
tokensUsed: 892
});
// When quota threshold hit
notifyWebhookEvent("quota.exhausted", {
accountId: "acct_42",
used: 999,
limit: 1000,
resetAt: "2024-01-16T00:00:00Z"
});
This function is non-blocking. OmniRoute will:
- Build the
WebhookPayloadwithevent,timestamp, anddata - Query enabled webhooks listening to this event
- Route
customwebhooks throughdeliverWebhook(signed + retried) - Route
slack/discord/telegramthroughdeliverRaw(unsigned, no retry)
Awaitable Delivery (Tests, CLI Tools)
Use dispatchEvent for synchronous result handling:
import { dispatchEvent } from "@/lib/webhookDispatcher";
const results = await dispatchEvent("model.completed", {
modelId: "claude-3-opus",
requestId: "xyz789"
});
// Results array contains success/failure per webhook
for (const r of results) {
console.log(r.webhookId, r.success, r.status, r.error);
}
Automatic Protection Against Flaky Endpoints
OmniRoute monitors delivery health via the webhookDeliveries table (managed in [src/lib/db/webhookDeliveries.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/webhookDeliveries.ts)). After each dispatch batch, the system calls disableWebhooksWithHighFailures(10) — any webhook with 10+ failures gets automatically disabled.
Check delivery history:
import { getWebhookDeliveries } from "@/lib/db/webhookDeliveries";
const recent = await getWebhookDeliveries(webhookId, { limit: 20 });
// recent[].status, recent[].latencyMs, recent[].error
Integration-Specific Payloads (Unsigned)
For Slack, Discord, and Telegram webhooks, OmniRoute bypasses HMAC and uses tailored formatters:
| Integration | Formatter Location | Notes |
|---|---|---|
| Slack | [src/lib/webhooks/integrations/slack.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/slack.ts) |
Block Kit message format |
| Discord | [src/lib/webhooks/integrations/discord.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/discord.ts) |
Embeds for rich formatting |
| Telegram | [src/lib/webhooks/integrations/telegram.ts](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/webhooks/integrations/telegram.ts) |
Bot API; decrypts token from metadata_encrypted |
These use deliverRaw (lines 39-68) which has no retry logic — integrations are expected to handle their own reliability.
Summary
- Enable HMAC signing by setting a
secretonkind: "custom"webhooks — OmniRoute addsX-Webhook-Signatureautomatically - Exponential backoff retries run 3 times by default with delays of 2ⁿ seconds
- Trigger events via
notifyWebhookEvent()for fire-and-forget ordispatchEvent()for awaitable results - Automatic disabling protects the system after 10 delivery failures per webhook
- Source files: Core logic in
src/lib/webhookDispatcher.ts, database layer insrc/lib/db/webhooks.tsandsrc/lib/db/webhookDeliveries.ts
Frequently Asked Questions
How do I verify the HMAC signature in my webhook handler?
Extract the X-Webhook-Signature header, remove the sha256= prefix, and compute HMAC-SHA256(body, secret) using a constant-time comparison. The payload is the raw request body bytes before any parsing — do not prettify or re-serialize the JSON.
Can I change the number of retry attempts?
The internal deliverWebhook function accepts a maxRetries parameter, but the public APIs (notifyWebhookEvent, dispatchEvent) use the default of 3. To customize, import deliverWebhook directly from src/lib/webhookDispatcher.ts and call it with your preferred retry count.
Why don't Slack/Discord/Telegram webhooks use HMAC signing?
These integrations use platform-specific authentication mechanisms (OAuth tokens, bot tokens) stored in metadata_encrypted. The deliverRaw helper handles these without HMAC since the receiving platform validates the token separately. Use kind: "custom" when you need signature verification on your own endpoint.
What happens if all retries fail?
The final attempt returns { success: false, status: 0 } (network error) or reflects the last HTTP status received. The failure is recorded in webhookDeliveries, counting toward the 10-failure threshold for automatic disabling. No further automatic action occurs — implement external alerting if critical webhooks fail persistently.
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 →