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:

json
{
"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:

text
${timestamp}.${rawRequestBody}

Reject timestamps more than five minutes from the receiver's clock and compare the complete v1= signature in constant time.

ts
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.

On this page