Agent skill

Ultravox Webhooks Skill

Receive and verify Ultravox webhooks. Use when setting up Ultravox webhook handlers, debugging X-Ultravox-Webhook-Signature / X-Ultravox-Webhook-Timestamp verification, or handling realtime voice-AI call lifecycle events like call.started, call.joined, call.ended, and call.billed.

Install this skill

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


Ultravox (ultravox.ai) is a realtime voice-AI platform. Account webhooks notify your server about the call lifecycle. Each delivery is a POST signed with HMAC-SHA256 using the webhook's secret.

When to Use This Skill

  • How do I receive Ultravox webhooks?
  • How do I verify Ultravox webhook signatures?
  • Why is my Ultravox webhook signature verification failing?
  • How do I handle call.started, call.joined, call.ended or call.billed events?
  • Why do I get a 401 for a webhook with several secrets, or none?

Secret Setup (important)

Give the Ultravox webhook exactly one secret and paste it here. A webhook without a secret is unsigned, and one with several secrets sends a list of signatures, so both fail verification.

Verification (core)

Headers: X-Ultravox-Webhook-Signature (bare lowercase hex digest, no prefix) and X-Ultravox-Webhook-Timestamp (ISO 8601). The signature is hex HMAC-SHA256, keyed with the secret (UTF-8 bytes, used as-is), over the raw body bytes followed directly by the timestamp header value, with no separator.

const HEX_DIGEST = /^[0-9a-fA-F]{64}$/;
// rawBody: Buffer from express.raw({ type: '*/*' }) - NOT parsed JSON
if (!sig || !ts || !HEX_DIGEST.test(sig)) return res.status(401).end();
const signed = Buffer.concat([rawBody, Buffer.from(ts, 'latin1')]); // body, then timestamp
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
const received = Buffer.from(sig.toLowerCase(), 'hex'); // safe: format validated above
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
  return res.status(401).end();
}
  • Compare the whole header as one digest. Do not split on commas or strip a prefix.
  • Missing/empty signature or timestamp, malformed value, or mismatch: respond 401.
  • Verify first, then JSON.parse. Never re-serialize the body.
  • Timestamp window is off by default. Hookdeck retries and replays resend the original timestamp, so a fixed window would reject legitimate retries. To opt in, set ULTRAVOX_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off); Ultravox's own example rejects anything older than 1 minute.
  • Respond 204 (Ultravox's recommendation). Errors and unavailability are retried 10 times, starting around 30 s and doubling each time; duplicates can occur, so handle events idempotently.

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

Common Event Types

Dispatch on the body's event field.

EventFires When
call.startedA call has started
call.joinedA participant has joined the call
call.endedA call has ended
call.billedA call has been billed

For payload details, see Ultravox webhook events.

Environment Variables

ULTRAVOX_WEBHOOK_SECRET=your_ultravox_webhook_secret_here   # the webhook's single secret
ULTRAVOX_WEBHOOK_TOLERANCE_SECONDS=0                       # optional; 0/unset = no timestamp check

Local Development

For local webhook testing, run the Hookdeck CLI via npx (no install, no account required):

npx hookdeck-cli listen 3000 ultravox --path /webhooks/ultravox

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →