How to Implement Webhooks in OmniRoute: Complete Developer Guide
To implement webhooks in OmniRoute, define a webhook record in the SQLite database using the REST API or CLI, specify the events you want to monitor (such as chat.completion or quota.exhausted), and select an integration type (Slack, Telegram, Discord, or custom). The dispatcher automatically routes matching events through the appropriate integration module, handles retries with exponential backoff, and disables failing webhooks after a configurable threshold.
OmniRoute provides a production-ready webhook subsystem that triggers external services based on internal events like model completions, quota changes, or MCP tool invocations. Whether you are building alerting for Slack or automated Telegram notifications, implementing webhooks in OmniRoute requires understanding its three-layer architecture: persistence, dispatch, and integration formatting. This guide references the actual source code from the diegosouzapw/OmniRoute repository to show you exactly how to configure, secure, and manage webhook deliveries.
Webhook Architecture Overview
OmniRoute organizes its webhook system into distinct layers that handle data storage, event routing, and payload formatting.
Persistence Layer
Webhook definitions live in SQLite via src/lib/db/webhooks.ts. This module exports the Webhook type that defines the schema:
// src/lib/db/webhooks.ts
export type Webhook = {
id: string; // auto-generated "wh-xxxxx"
url: string; // destination endpoint
description?: string;
enabled: boolean;
events: string[]; // e.g. ["chat.completion", "quota.exhausted"]
integration: "telegram" | "slack" | "discord" | "custom";
};
Delivery logs are tracked separately in src/lib/db/webhookDeliveries.ts, which records every attempt with timestamps, success flags, and response metadata.
Dispatcher Engine
The core delivery logic resides in src/lib/webhookDispatcher.ts. The deliverWebhook function selects enabled hooks matching the event type, builds service-specific payloads, and executes safe HTTP POST requests:
// src/lib/webhookDispatcher.ts
async function deliverWebhook(event: WebhookEvent) {
const targets = db.getEnabledWebhooks().filter(w => w.events.includes(event.type));
for (const hook of targets) {
const payload = buildPayload(hook.integration, event);
await postWithRetry(hook.url, payload, hook.id);
}
}
All outbound requests pass a strict SSRF guard implemented in the same file, preventing attacks against internal network ranges. The postWithRetry utility implements exponential backoff and logs outcomes to the deliveries table.
Integration Modules
Payload formatting is delegated to tiny integration helpers in src/lib/webhooks/integrations/:
- Slack:
src/lib/webhooks/integrations/slack.tsformats blocks and mrkdwn text. - Telegram:
src/lib/webhooks/integrations/telegram.tsconstructs the JSON body expected by the Telegram Bot API. - Discord:
src/lib/webhooks/integrations/discord.tsstructures embeds and content fields. - Custom:
src/lib/webhooks/integrations/custom.tshandles raw JSON templates with optional placeholder interpolation.
Step-by-Step Implementation Guide
1. Define the Webhook Record
Create a webhook using the REST API at src/app/api/webhooks/route.ts or the CLI command defined in bin/cli/commands/webhooks.mjs:
omniroute webhooks add \
--url https://hooks.slack.com/services/T000/B000/XXXXXXXX \
--integration slack \
--events chat.completion,quota.exhausted \
--description "Notify Slack on completions & quota"
Alternatively, POST directly to /api/webhooks:
POST /api/webhooks HTTP/1.1
Content-Type: application/json
{
"url": "https://hooks.slack.com/services/T000/B000/XXXXXXXX",
"integration": "slack",
"events": ["chat.completion", "model.error"],
"description": "Slack notifications for model activity"
}
2. Select Event Types
Available events are catalogued in src/lib/webhooks/eventDescriptions.ts. Common identifiers include:
chat.completion– Fired after SSE response completion insrc/open-sse/handlers/chatCore.ts.quota.exhausted– Triggered insrc/lib/db/creditBalance.tsafter balance updates.mcp.tool.invoked– Emitted fromsrc/open-sse/mcp-server/server.tsduring tool execution.
Add these strings to the webhook's events array to subscribe.
3. Configure Custom Payloads (Optional)
For non-standard integrations, store a JSON template in the customPayload column (added by migration 029_webhooks_custom_payload.sql). The dispatcher passes this template to buildCustomPayload in src/lib/webhooks/integrations/custom.ts, which performs simple interpolation:
// src/lib/webhooks/integrations/custom.ts
export function buildCustomPayload(event: WebhookEvent, template: any) {
const json = JSON.stringify(template).replace("{{detail}}", event.detail);
return JSON.parse(json);
}
Set integration: "custom" and include your template in the database record to use this feature.
4. Test the Webhook
Use the diagnostic endpoint defined in src/app/api/webhooks/[id]/route.ts:
curl -X POST https://your-omniroute-instance/api/webhooks/wh-00abc123/test
The CLI provides a convenience wrapper:
omniroute webhooks test wh-00abc123
This triggers an immediate dispatch with a static "test" payload, bypassing the event pipeline to verify connectivity and formatting.
Integration-Specific Payload Examples
Slack
The buildSlackPayload function in src/lib/webhooks/integrations/slack.ts constructs message blocks:
// src/lib/webhooks/integrations/slack.ts
export function buildSlackPayload(event: WebhookEvent) {
return {
text: `*${event.type}* – ${event.summary}`,
blocks: [{
type: "section",
text: { type: "mrkdwn", text: event.detail }
}],
};
}
Telegram
Telegram integration formats the payload for the Bot API sendMessage endpoint:
// src/lib/webhooks/integrations/telegram.ts
export function buildTelegramPayload(event: WebhookEvent) {
return {
text: `<b>${event.type}</b>\n${event.detail}`,
parse_mode: "HTML"
};
}
Discord
Discord webhooks receive embed-compatible JSON:
// src/lib/webhooks/integrations/discord.ts
export function buildDiscordPayload(event: WebhookEvent) {
return {
content: `Event: ${event.type}`,
embeds: [{
title: event.summary,
description: event.detail,
timestamp: new Date().toISOString()
}]
};
}
Manual Event Dispatch
You can trigger webhooks programmatically from your own application logic by importing the dispatcher:
import { deliverWebhook } from "./src/lib/webhookDispatcher";
await deliverWebhook({
type: "chat.completion",
summary: "User X completed a request",
detail: "Model: anthropic-1.3b, tokens: 274/1024",
payload: { /* optional additional data */ }
});
This is useful for custom instrumentation or extending OmniRoute with proprietary event types.
Security and Failure Handling
SSRF Protection
Every outbound request is validated by the SSRF guard in src/lib/webhookDispatcher.ts. The implementation rejects private IP ranges and internal hostnames unless explicitly allowlisted. See tests/unit/api/webhooks/webhook-url-ssrf-guard.test.ts for the specific validation rules.
Automatic Disable on Failure
A background service in src/lib/services/webhookFailureMonitor.ts runs every minute to check delivery outcomes. If a webhook exceeds the default threshold of 5 consecutive failures, the monitor flips enabled to false in the database. Administrators can re-enable the webhook via the API or CLI after resolving the endpoint issue.
Summary
- Define webhooks in SQLite using
src/lib/db/webhooks.tsvia the REST API (/api/webhooks) or CLI (omniroute webhooks add). - Select events from the catalogue in
src/lib/webhooks/eventDescriptions.tssuch aschat.completionorquota.exhausted. - Choose integrations (Slack, Telegram, Discord, or custom) to automatically format payloads via modules in
src/lib/webhooks/integrations/. - Test deliveries using the
/api/webhooks/:id/testendpoint or CLI test command. - Rely on built-in safeguards including SSRF protection in
src/lib/webhookDispatcher.tsand automatic disable logic insrc/lib/services/webhookFailureMonitor.ts.
Frequently Asked Questions
What events can I subscribe to in OmniRoute webhooks?
OmniRoute emits typed events throughout its request pipeline, including chat.completion (fired after model responses), quota.exhausted (triggered on balance updates), and mcp.tool.invoked (logged during MCP tool execution). The complete catalogue is maintained in src/lib/webhooks/eventDescriptions.ts with stable identifiers and sample payloads.
How does OmniRoute handle webhook failures and retries?
The postWithRetry function in src/lib/webhookDispatcher.ts implements exponential backoff for failed HTTP requests. Each attempt is logged to src/lib/db/webhookDeliveries.ts. A background monitor (src/lib/services/webhookFailureMonitor.ts) counts consecutive failures and automatically disables the webhook after reaching the configurable threshold (default 5 errors).
Can I use custom webhook endpoints outside of Slack, Telegram, or Discord?
Yes. Set the integration field to "custom" and provide a JSON template in the customPayload column (added by migration 029_webhooks_custom_payload.sql). The dispatcher sends the raw JSON unchanged after optional placeholder interpolation via src/lib/webhooks/integrations/custom.ts.
Where are webhook delivery logs stored in OmniRoute?
Delivery logs reside in the SQLite database table managed by src/lib/db/webhookDeliveries.ts. Each record contains the webhook ID, timestamp, success status, HTTP response code, and error message, enabling audit trails and debugging of failed deliveries.
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 →