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_. 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_resultormodel_outputevents? - 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:
| Event | Triggered When |
|---|---|
agent_input | Input (user content, etc.) is added to an agent's timeline |
model_output | The model produces output (stop_reason, content_blocks) |
tool_result | A tool call finishes (tool_call_id, outcome) |
context_checkpoint | The context is summarized (summary, summarized_) |
tool_call_update | A 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
- references/overview.md - Omnara webhook concepts, payloads, events, delivery
- references/setup.md - Configure
event_webhook, create the signing secret - references/verification.md - Verification details, header fallback, manual verifier, timestamp window