Webhooks
Every event Omniio will post, the payload shape, and how to verify a delivery came from us.
Webhooks push an Activity row to a URL you own as the tool call finishes. Each delivery is signed independently so the receiver can prove it came from Omniio.
Events#
There are two tool-call events:
- tool_call.succeeded
- An upstream tool ran and returned a result.
- tool_call.failed
- Omniio refused the call, or the upstream returned an error.
Discovery calls do not fire webhooks because they do not create Activity rows. Each endpoint subscribes to one or both events. An account may keep up to five endpoints, with one registration per URL and a separate secret for each.
Delivery contract#
- Transport
- One HTTPS POST with a UTF-8 JSON body. HTTP, localhost, internal names and private addresses are refused.
- Success
- Any 2xx response.
- Timeout
- 8 seconds. Return quickly and enqueue downstream work.
- Redirects
- Never followed; a 3xx is a failed attempt rather than a signature re-presented to another URL.
- Ordering
- Not guaranteed. Sort on `createdAt` when order matters.
- Delivery
- At most once. There is no automatic retry queue.
The payload uses the same stored call shape as GET /activity:
{ "id": "0f4a6f0c-1b7e-4a3f-9c21-6d8a1f5b2c34", "event": "tool_call.succeeded", "createdAt": "2026-01-14T09:31:04.812Z", "data": { "clientId": "claude-code", "server": "GitHub", "serverSlug": "github", "tool": "create_pull_request", "qualifiedName": "github__create_pull_request", "ok": true, "durationMs": 812, "error": null, "arguments": {"owner": "acme", "repo": "web"}, "result": {"url": "https://github.com/acme/web/pull/412"}, "redacted": [], "injectionFindings": [] }}Payloads reflect the retained copy: recognised sensitive values are redacted and arguments and results are each capped at 64 KB. Treat a receiver as holding the same class of data as the Activity trail.
Headers#
- omniio-eventstring
- The event name, duplicated from the body so routing need not parse it first.
- omniio-deliveryUUID
- Unique to this attempt; log it when reporting delivery trouble.
- omniio-timestampUnix seconds
- Time of signing and part of the signed message.
- omniio-signaturev1=<hex>
- HMAC-SHA256 over the timestamp and raw body.
Content-Type is application/json; the user agent is
Omniio (+https://omniio.dev). A user agent is useful to route but is not proof
of origin.
Verify the signature#
The signing secret starts with whsec_ followed by 32 random bytes encoded as
base64url. It is shown once when the endpoint is created. The signed string is:
${timestamp}.${rawRequestBody}Reject timestamps more than five minutes from the receiver's clock and compare
the complete v1= signature in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";export function verify(headers, body, secret) { const signature = headers["omniio-signature"] ?? ""; const timestamp = Number(headers["omniio-timestamp"]); const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp); if (!Number.isFinite(timestamp) || age > 300) return false; const mac = createHmac("sha256", secret) .update(`${timestamp}.${body}`) .digest("hex"); const expected = Buffer.from(`v1=${mac}`); const received = Buffer.from(signature); return expected.length === received.length && timingSafeEqual(expected, received);}Verify the raw request bytes before JSON parsing. Parsing and serialising again can change key order, whitespace or number formatting, so it is not the string Omniio signed.
The timestamp is inside the HMAC. Rewriting it to the current time therefore invalidates a captured signature instead of extending it forever.
Failures and recovery#
Omniio stores the last attempt, status or network error, and consecutive failure count. A success resets the count. After 20 consecutive failures, the endpoint switches itself off; enabling it again clears the failure count.
Because there are no retries, reconcile a receiver outage with GET /activity
from the last processed createdAt. The audit trail is the durable copy.
Each endpoint can send a real signed test event to exercise DNS, TLS and the
receiver's signature check. A receiver should handle or ignore event: "test"
without trying to read tool-call fields from its data object.
Manage endpoints#
Settings → Webhooks creates, pauses, tests and deletes endpoints, changes event subscriptions, and replaces signing secrets. A replacement invalidates the old secret immediately. Endpoint management is not available through the REST API because an API key should not be able to mint a new credential.
Next: Telemetry export.