mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
Every .md under docs/{architecture,guides,reference,frameworks,routing,
security,compression,ops,diagrams} plus docs/README.md now opens with:
---
title: "<inferred from first H1>"
version: 3.8.0
lastUpdated: 2026-05-13
---
46 files updated (no docs were skipped — none had pre-existing
frontmatter). [slug]/page.tsx already reads frontmatter.version and
frontmatter.lastUpdated via gray-matter and renders a "v3.8.0" pill
plus a "Last updated" caption, so the UI picks these up automatically.
Helper: scripts/docs/add-frontmatter.mjs — idempotent (skips files that
already start with `---`), falls back to a humanized basename when no
leading H1 exists. Excludes docs/i18n/, docs/screenshots/,
docs/superpowers/, docs/diagrams/exported/. Re-runnable safely.
Also regenerated src/app/docs/lib/docs-auto-generated.ts: 44 docs across
8 sections (Architecture / Guides / Reference / Frameworks / Routing /
Security / Compression / Ops), which now includes the 14 docs that were
missing from the v3.7 sidebar (Cloud Agents, Guardrails, Memory, Skills,
Webhooks, Evals, Authz, Agent Protocols, Repository Map, Provider
Reference, Reasoning Replay, Stealth Guide, Tunnels Guide, Electron
Guide).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
260 lines
9.3 KiB
Markdown
260 lines
9.3 KiB
Markdown
---
|
|
title: "Webhooks"
|
|
version: 3.8.0
|
|
lastUpdated: 2026-05-13
|
|
---
|
|
|
|
# Webhooks
|
|
|
|
> **Source of truth:** `src/lib/webhookDispatcher.ts`, `src/lib/db/webhooks.ts`, `src/app/api/webhooks/`
|
|
> **Last updated:** 2026-05-13 — v3.8.0
|
|
|
|
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
|
|
|
|
The `WebhookEvent` type (`src/lib/webhookDispatcher.ts`) currently models:
|
|
|
|
| Event | Fires when |
|
|
| -------------------- | --------------------------------------------------------- |
|
|
| `request.completed` | A proxied request completes successfully |
|
|
| `request.failed` | A proxied request fails after all retries/fallback |
|
|
| `provider.error` | A provider returns an error eligible for circuit-breaking |
|
|
| `provider.recovered` | A previously failing provider returns to a healthy state |
|
|
| `quota.exceeded` | An API key crosses a budget/quota threshold |
|
|
| `combo.switched` | A combo strategy switches its primary target |
|
|
| `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.ping` events are still landing. Check `grep dispatchEvent` to see
|
|
> which paths currently invoke the dispatcher in your release.
|
|
|
|
## 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
|
|
|
|
When a webhook has a `secret`, OmniRoute signs the JSON body and sends:
|
|
|
|
```
|
|
Content-Type: application/json
|
|
User-Agent: OmniRoute-Webhook/1.0
|
|
X-Webhook-Event: <event>
|
|
X-Webhook-Timestamp: <ISO-8601>
|
|
X-Webhook-Signature: sha256=<hex HMAC-SHA256(secret, body)>
|
|
```
|
|
|
|
> Header names use the `X-Webhook-*` prefix (not `X-OmniRoute-*`). The signature
|
|
> value is `sha256=<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
|
|
|
|
```typescript
|
|
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
|
|
|
|
`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 increments `failure_count`.
|
|
- The dispatcher calls `disableWebhooksWithHighFailures(10)` after each fan-out,
|
|
so any webhook with `failure_count >= 10` is automatically disabled.
|
|
|
|
## 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 |
|
|
|
|
There is **no separate `webhook_deliveries` table** in the current schema —
|
|
delivery history is aggregated on the `webhooks` row. If you need full audit
|
|
history, consume `request.completed` / `audit` style events from a downstream
|
|
log store.
|
|
|
|
## 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) |
|
|
|
|
`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
|
|
|
|
```bash
|
|
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", "provider.error"],
|
|
"description": "Slack alerts"
|
|
}'
|
|
```
|
|
|
|
If `secret` is omitted, the server generates a `whsec_<hex>` secret and returns
|
|
it in the response.
|
|
|
|
### Test webhook
|
|
|
|
```bash
|
|
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
|
|
|
|
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`, and `last_status`
|
|
- One-click test delivery
|
|
- Manual enable/disable toggle
|
|
|
|
## Payload Examples
|
|
|
|
### request.completed
|
|
|
|
```json
|
|
{
|
|
"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
|
|
}
|
|
}
|
|
```
|
|
|
|
### provider.error
|
|
|
|
```json
|
|
{
|
|
"event": "provider.error",
|
|
"timestamp": "2026-05-13T20:31:00.000Z",
|
|
"data": {
|
|
"provider": "anthropic",
|
|
"status": 503,
|
|
"consecutive_failures": 5,
|
|
"circuit_state": "open"
|
|
}
|
|
}
|
|
```
|
|
|
|
### test.ping
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
- **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 calling `PUT /api/webhooks/[id]` with `enabled: true`
|
|
after fixing the receiver.
|
|
- **Rotate secrets periodically** — `PUT` a new `secret`, deploy the new value
|
|
to the receiver, and confirm via the test endpoint.
|
|
|
|
## See Also
|
|
|
|
- [API_REFERENCE.md](../reference/API_REFERENCE.md) — full management API surface
|
|
- [RESILIENCE_GUIDE.md](../architecture/RESILIENCE_GUIDE.md) — circuit breaker / cooldown
|
|
semantics that drive `provider.error` / `provider.recovered`
|
|
- Source: `src/lib/webhookDispatcher.ts`, `src/lib/db/webhooks.ts`
|