---
title: Webhooks (receive HTTP POSTs into your inbox)
---

# Webhooks (receive HTTP POSTs into your inbox)

## What it is

Omniscio's webhook integration runs a local HTTP server that accepts incoming POST requests and surfaces each one as a row in the unified inbox — alongside Gmail, SMS, Slack, Telegram, and RSS. Each configured **webhook source** has a unique URL path and bearer token; every POST to that path becomes a "message" in that source's "conversation". This is a **receive-only** channel — there's no replying. Typical use cases: GitHub push/issue notifications, Stripe payment events, Zapier/Make pipelines, custom scripts that want to ping you, or any third-party service that supports outgoing webhooks.

## Where to find it

**Settings → Webhooks** carries the master toggle and the port the local server listens on, and each source you add there produces the URL and bearer token you hand to the sender. What arrives lands in the **inbox** as its own conversation, and the same payloads are visible to the automation engine and to the app-wide search.

## How it behaves

### How to use it

1. **Enable webhooks.** Settings → **Webhooks** → toggle `webhooksEnabled` on. Pick the port the local HTTP server listens on (configurable via `webhookPort` in config-store).
2. **Add a webhook source.** Each source gets a unique `sourceId` (URL slug) and a bearer token Omniscio auto-generates. Copy the resulting webhook URL — `http://<your-host>:<port>/hooks/<sourceId>` — into whatever third-party service is going to POST to it. The `/hooks/` path segment is required: the server only answers `POST /hooks/:sourceId`, and any path without it returns a hard `404 Not found`.
3. **Test it.** From a terminal, `curl -X POST -H "Authorization: Bearer <token>" -d '{"hello":"world"}' http://localhost:<port>/hooks/<sourceId>`. Within a second the request should appear as a new row in the Omniscio inbox under that source's conversation.
4. **Read in the inbox.** Click a webhook row to see the full conversation panel — every POST received shows the request method, headers, formatted JSON body (auto-pretty-printed), receive timestamp, and remote IP.
5. **Pair with automations.** Webhook payloads flow through the standard Automation engine — keyword-match the body, route to email forward, summarize in the daily digest, or trigger a Claude session to react to the event.

## For agents

### How it works

The HTTP server is [/src/main/services/webhook-server.ts](/src/main/services/webhook-server.ts) — built on Node's stdlib `http.createServer` (no Express dependency) with a 1 MB max body size and a 10-second body-read timeout to prevent slow-loris attacks. Source validation is dependency-injected via `setSourceValidator()` so the server doesn't import DB queries directly: each incoming request looks up its `sourceId`, refuses with 404 (`Source not found`) if missing, then verifies the `Authorization: Bearer <token>` header matches the stored token (a missing or mismatched token is a 401 `Unauthorized`). Successful POSTs build a `WebhookPayload` (sourceId, method, headers, body, receivedAt ISO timestamp, remoteAddress) and fan out to listeners registered with `onWebhookReceived()`. The payload also carries an optional `bodyBase64` field alongside `body` when the raw bytes aren't clean UTF-8, so a binary or mis-encoded payload round-trips losslessly. **Duplicate suppression:** if a redelivered event carries a delivery-id header (`x-github-delivery`, `idempotency-key`, `x-idempotency-key`, `x-hook-id`, `x-webhook-id`, `x-event-id`, or `x-request-id`) already seen within the last 10 minutes, the server acks it `200 {ok: true, received: true, duplicate: true}` but does **not** fan out to listeners or create a second inbox row — a sender's at-least-once retries never double-post. A POST with **no** delivery-id header always fans out (at-least-once delivery — your listeners must be idempotent). The unified-inbox bridge is [/src/main/services/channels/webhook-adapter.ts](/src/main/services/channels/webhook-adapter.ts), which translates each payload into a `UnifiedMessage` whose body is pretty-printed JSON when parseable (raw text otherwise). The adapter declares `canSend: false` so reply UI is hidden. Persistence: [/src/main/db/queries-webhook.ts](/src/main/db/queries-webhook.ts). Settings flag: `webhooksEnabled` in [/src/shared/types.ts](/src/shared/types.ts); the virtual-project constant `WEBHOOK_PROJECT_ID = '__webhook__'` lets the inbox group webhook items together. Search integration: `SearchChannelType` in `types.ts` includes `'webhook'`. Response codes a sender should expect: `200` on success; `404 Not found` for a path missing the `/hooks/` segment; `404 Source not found` for an unknown `sourceId`; `405` (with an `Allow: POST` header) for a non-POST method; `401 Unauthorized` for a missing/mismatched bearer; `403` for a browser cross-site (`Sec-Fetch-Site: cross-site`) request; **`429 Rate limit exceeded`** (with a `Retry-After` header) once a single source exceeds 60 authenticated requests per 60 seconds — a busy sender WILL hit this and **must honor `Retry-After` and retry, not drop the event**; **`413 Payload too large`** for a body over the 1 MB cap; **`408 Request timeout`** if the body read stalls past the 10-second slow-loris timeout; and **`500`** in two distinct cases: `Source validator not configured` when the server isn't fully wired, or `Internal server error` when an unexpected error occurs while reading the body. The `413 Payload too large`, `408 Request timeout`, and the **internal-error** `500` (`Internal server error`) responses additionally include a `requestId` in their body you can quote when reporting a failed delivery; the other error codes — **including the config-error `500` (`Source validator not configured`)** — do not. Every response body — success or error — also carries a boolean `ok` field (`true` for a 2xx status, `false` otherwise) as its leading discriminator, e.g. a successful post acks `200 {ok: true, ...}`. Separately, the same server answers **`GET /health`** → `200 {ok: true, status:'ok'}` when fully wired, or `503 {ok: false, status:'unavailable'}` when the source validator (or listener) isn't up.

## Related

- [INDEX.md](INDEX.md) — full library index
- [rss-integration.md](rss-integration.md) — sibling read-only channel for pull-based feeds
- [automations-and-auto-replies.md](automations-and-auto-replies.md) — keyword rules that route webhook payloads
- [daily-digest.md](daily-digest.md) — webhook events can roll up into the once-a-day briefing
- [global-search.md](global-search.md) — Ctrl+K searches webhook bodies alongside every other channel
