Agent skill

Omnara Webhooks Skill

Receive and verify Omnara webhooks (hosted AI agents, Standard Webhooks signing). Use when setting up an Omnara agent event_webhook handler, debugging webhook-signature verification, or handling agent_input, model_output, tool_result, context_checkpoint and tool_call_update events.

Install this skill

npx skills add hookdeck/webhook-skills --skill omnara-webhooks


An Omnara agent's event_webhook sends its timeline events and tool-call updates to a public HTTPS endpoint as JSON POST requests. The body is { "event": "<type>", "data": {...} }: the event type is the top-level event, and timeline events repeat it in data.event_kind. Every request carries Webhook-Id (stable across retries) and Webhook-Timestamp; signed requests also carry Webhook-Signature, per Standard Webhooks.

Signing is opt-in

Deliveries are signed only when the agent config sets event_webhook.signing_secret_id. Without it they still carry Webhook-Id and Webhook-Timestamp but cannot be verified. Hookdeck's configuration alert for this source reads exactly:

"Signing is opt-in per agent: set event_webhook.signing_secret_id, then paste that secret here as whsec_ followed by the secret. Unsigned deliveries fail verification."

See references/setup.md.

When to Use This Skill

  • How do I receive Omnara webhooks (event_webhook)?
  • How do I verify Omnara webhook signatures (webhook-signature)?
  • How do I handle tool_call_update, tool_result or model_output events?
  • Why is my Omnara webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, signed with HMAC-SHA256 and base64-encoded. webhook-signature holds one or more space-separated v1,<base64> entries. The secret is bare base64 (from openssl rand -base64 32); the HMAC key is its base64-decoding. The official standardwebhooks library (npm and pip) checks every v1 entry in constant time, strips a whsec_ prefix if present, and otherwise base64-decodes the whole secret, so the bare value and the whsec_-prefixed form Hookdeck needs give the same key. Read the webhook-* headers and, when webhook-id is absent, fall back to the svix-* headers (svix-id, svix-timestamp, svix-signature), as Hookdeck does.

const { Webhook } = require('standardwebhooks');

const prefix = req.headers['webhook-id'] ? 'webhook' : 'svix';
// rawBody: the raw Buffer/string, never re-serialized JSON
const payload = new Webhook(process.env.OMNARA_WEBHOOK_SECRET).verify(rawBody, {
  'webhook-id': req.headers[`${prefix}-id`],
  'webhook-timestamp': req.headers[`${prefix}-timestamp`],
  'webhook-signature': req.headers[`${prefix}-signature`],
}); // throws on a bad signature or a timestamp outside the window; returns the parsed JSON
from standardwebhooks.webhooks import Webhook

prefix = "webhook" if headers.get("webhook-id") else "svix"
payload = Webhook(os.environ["OMNARA_WEBHOOK_SECRET"]).verify(raw_body, {
    "webhook-id": headers.get(f"{prefix}-id"),
    "webhook-timestamp": headers.get(f"{prefix}-timestamp"),
    "webhook-signature": headers.get(f"{prefix}-signature"),
})

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Keep the secret exactly as Omnara shows it in OMNARA_WEBHOOK_SECRET. Handlers fail closed: with the variable unset they return 500. A missing header or bad signature gets 400. With Express, read the body with express.raw({ type: '*/*' }) so any content type reaches the verifier as the exact bytes Omnara signed, and parse JSON only after the check.

Hookdeck retries and replays hit the 5-minute window

The standardwebhooks library always enforces a fixed 5-minute timestamp window that can't be configured. Hookdeck retries and replays resend the original webhook-timestamp, so the library rejects any retry or replay more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on webhook-id instead of relying on the timestamp.

Common Event Types

5 event types are documented (the list may not be exhaustive). The name is event in the body, and event_webhook.events is a required, nonempty allowlist of them. The examples handle all five:

EventTriggered When
agent_inputInput (user content, etc.) is added to an agent's timeline
model_outputThe model produces output (stop_reason, content_blocks)
tool_resultA tool call finishes (tool_call_id, outcome)
context_checkpointThe context is summarized (summary, summarized_through_event_sequence)
tool_call_updateA tool call or interaction changes state (a notification to fetch the details)

Delivery is at-least-once and unordered; see references/overview.md for payload shapes, retries and deduplication.

Environment Variables

# Signing secret exactly as stored in Omnara (bare base64; a whsec_ prefix also works)
OMNARA_WEBHOOK_SECRET=your_base64_secret

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 omnara --path /webhooks/omnara

Set the agent's event_webhook.url to the tunnel URL, with events and signing_secret_id set.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →