Webhooks
Source of truth:
src/lib/webhookDispatcher.ts,src/lib/db/webhooks.ts,src/app/api/webhooks/Last updated: 2026-06-28 — v3.8.40
OmniRoute can fire HTTP webhooks on platform events. Use them to integrate with Slack, PagerDuty, Datadog, internal alerting services, or any HTTP receiver.
The dispatcher signs each delivery with HMAC-SHA256, retries on transient failures, tracks delivery health per webhook, and auto-disables endpoints that keep failing.
Supported Events
Section titled “Supported Events”The WebhookEvent type (src/lib/webhooks/eventDescriptions.ts, consumed by src/lib/webhookDispatcher.ts) currently models exactly four events:
| Event | Fires when |
|---|---|
request.completed |
A proxied request completes successfully |
request.failed |
A proxied request fails after all retries/fallback |
quota.exceeded |
An API key crosses a budget/quota threshold |
test.ping |
Synthetic event used by the test endpoint |
Subscriptions accept the literal "*" to receive every event. Unknown event
names in events are ignored at dispatch time.
Note: the dispatcher API is wired, but production call sites for some of the non-
test.pingevents are still landing. Checkgrep dispatchEventto see which paths currently invoke the dispatcher in your release.
Architecture
Section titled “Architecture”Caller (handler, service, monitor) dispatchEvent(event, data) [src/lib/webhookDispatcher.ts] -> getEnabledWebhooks() [src/lib/db/webhooks.ts] -> filter by webhook.events -> for each match (in parallel): deliverWebhook(url, payload, secret) build payload { event, timestamp, data } sign body with HMAC-SHA256 (if secret present) POST with 10s timeout retry up to 3 times on 5xx / network error recordWebhookDelivery(id, status, success) -> disableWebhooksWithHighFailures(10)Dispatch is fire-and-forget for the caller: Promise.allSettled swallows
per-webhook errors so one bad receiver cannot block the others.
HMAC Signing
Section titled “HMAC Signing”When a webhook has a secret, OmniRoute signs the JSON body and sends:
Content-Type: application/jsonUser-Agent: OmniRoute-Webhook/1.0X-Webhook-Event: <event>X-Webhook-Timestamp: <ISO-8601>X-Webhook-Signature: sha256=<hex HMAC-SHA256(secret, body)>Header names use the
X-Webhook-*prefix (notX-OmniRoute-*). The signature value issha256=<hex>— verify the full prefix.
If createWebhook is called without a secret, the DB module generates one
(whsec_<48 hex>) so all webhooks are signed by default.
Verifying on the receiver
Section titled “Verifying on the receiver”import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, signature: string, secret: string) { const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b);}Always verify against the raw request body, before any JSON parsing.
Retry & Failure Policy
Section titled “Retry & Failure Policy”deliverWebhook(url, payload, secret, maxRetries = 3):
- 10 second timeout per attempt (
AbortController). - HTTP 2xx counts as success.
- HTTP 3xx/4xx counts as a non-retryable final status — recorded as delivered
with
success = res.ok. - HTTP 5xx and network errors are retried with exponential backoff:
2^attempt * 1000 ms(1s, 2s, 4s). - After
maxRetries, the delivery is recorded as failed. - Each delivery updates
last_triggered_at,last_status, and either resets or incrementsfailure_count. - The dispatcher calls
disableWebhooksWithHighFailures(10)after each fan-out, so any webhook withfailure_count >= 10is automatically disabled.
Database
Section titled “Database”Table webhooks (migration 011_webhooks.sql):
| Column | Type | Notes |
|---|---|---|
id |
TEXT PK | UUID |
url |
TEXT | Destination URL |
events |
TEXT | JSON array; default ["*"] |
secret |
TEXT | HMAC secret (auto-generated if not given) |
enabled |
INT | 0/1; defaults to 1 |
description |
TEXT | Optional human label |
created_at |
TEXT | datetime('now') |
last_triggered_at |
TEXT | Updated on every delivery attempt |
last_status |
INT | HTTP status of the last attempt (0 = network) |
failure_count |
INT | Resets to 0 on success, +1 on failure |
Delivery history is persisted in the dedicated webhook_deliveries table
(migration 069_webhook_deliveries.sql, written via
src/lib/db/webhookDeliveries.ts::insertDelivery on every attempt), in addition
to the aggregate counters on the webhooks row. Kind metadata (Slack / Discord /
Telegram / custom payload transformers) was added by 070_webhooks_kind_metadata.sql.
REST API
Section titled “REST API”All endpoints require management auth (requireManagementAuth).
| Endpoint | Method | Description |
|---|---|---|
/api/webhooks |
GET | List webhooks (secrets masked) |
/api/webhooks |
POST | Create webhook |
/api/webhooks/[id] |
GET | Webhook detail (full secret) |
/api/webhooks/[id] |
PUT | Update fields |
/api/webhooks/[id] |
DELETE | Remove |
/api/webhooks/[id]/test |
POST | Fire a test.ping (no retries) |
/api/webhooks/[id]/deliveries |
GET | Recent delivery attempts for one webhook |
/api/webhooks/validate-url |
POST | Pre-flight URL validation (SSRF guard) |
GET /api/webhooks masks the secret to <first 10 chars>... to avoid leaking
on listing pages. Use the [id] GET when you actually need the secret.
Create webhook
Section titled “Create webhook”curl -X POST http://localhost:20128/api/webhooks \ -H "Cookie: auth_token=..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.slack.com/services/...", "secret": "whsec_my_shared_secret", "events": ["quota.exceeded", "request.failed"], "description": "Slack alerts" }'If secret is omitted, the server generates a whsec_<hex> secret and returns
it in the response.
Test webhook
Section titled “Test webhook”curl -X POST http://localhost:20128/api/webhooks/<id>/test \ -H "Cookie: auth_token=..."Returns { delivered, status, error }. No retries are attempted — useful for
quickly validating that the receiver accepts the payload and signature.
Dashboard
Section titled “Dashboard”The dashboard page at /dashboard/webhooks (see
src/app/(dashboard)/dashboard/webhooks/page.tsx) provides:
- Create/edit webhooks with an event picker
- Status indicator (active / inactive / errored) based on
enabled,failure_count, andlast_status - One-click test delivery
- Manual enable/disable toggle
Payload Examples
Section titled “Payload Examples”request.completed
Section titled “request.completed”{ "event": "request.completed", "timestamp": "2026-05-13T20:30:00.123Z", "data": { "trace_id": "...", "api_key_id": "...", "provider": "openai", "model": "gpt-5", "status": 200, "tokens_in": 142, "tokens_out": 350, "cost_usd": 0.0042 }}test.ping
Section titled “test.ping”{ "event": "test.ping", "timestamp": "2026-05-13T20:32:00.000Z", "data": { "message": "Test webhook delivery from OmniRoute", "webhookId": "<uuid>" }}Field shapes for non-test.ping events are defined by the call sites that emit
them; treat the data object as forward-compatible (add fields, don’t depend on
absence).
Best Practices
Section titled “Best Practices”- Verify the signature on every delivery against the raw body — prevents spoofed POSTs from anyone who guesses your webhook URL.
- Respond 2xx within ~5 seconds — the dispatcher times out at 10 s. Slow
receivers will eat retries and inflate
failure_count. - Make handlers idempotent — retries and at-least-once delivery semantics mean duplicates are possible.
- Subscribe minimally — list only events you actually consume;
"*"will add cost on receivers you do not control. - Watch
failure_count— endpoints are auto-disabled at 10 consecutive failures; reset by callingPUT /api/webhooks/[id]withenabled: trueafter fixing the receiver. - Rotate secrets periodically —
PUTa newsecret, deploy the new value to the receiver, and confirm via the test endpoint.
See Also
Section titled “See Also”- API_REFERENCE.md — full management API surface
- RESILIENCE_GUIDE.md — circuit breaker / cooldown
semantics behind provider failures surfaced via
request.failed - Source:
src/lib/webhookDispatcher.ts,src/lib/db/webhooks.ts
HagiCode
HagiCode is an agentic coding workspace. Rich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate, creating an engaging development experience.
Explore an engaging development experience through rich, convenient UI interactions.
- SmartOpenSpec workflows turn intent into an executable path, from proposal to design, tasks, and a shipped change.
- EfficientMulti-agent execution keeps research, implementation, and review moving in parallel, across several repositories and the Agent CLIs you already use.
- InterestingRich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate.
Download for Windows
Share HagiCode
Turn ideas into shipped software.
HagiCode is an agentic coding workspace. OpenSpec workflows plan the change, multi-agent execution carries it out, and Hero Scenario makes the long run visible, so ideas become shipped software.
- Smart
- OpenSpec workflows turn intent into a path from proposal to shipped change.
- Efficient
- Multi-agent execution runs research, build, and review in parallel.
- Fun
- Rich UI interactions make planning, agent work, and projects easier to follow.
© 2026 HagiCode | Powered By hagilight-starlight@0.21.0 | Branch: main | Commit: de60bcd | Built:

