Agent skill

Hume Webhooks Skill

Receive and verify Hume EVI (speech-to-speech) webhooks. Use when setting up a Hume webhook handler, debugging X-Hume-AI-Webhook-Signature HMAC-SHA256 signature verification, or handling chat_started, chat_ended and tool_call events from Hume's emotionally intelligent voice AI.

Install this skill

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


Webhooks from Hume AI (hume.ai), the emotionally intelligent voice AI company. This skill covers EVI (speech-to-speech) webhooks only, which are configured on an EVI Config and report the lifecycle of EVI Chat sessions.

Setup Note

For Hume EVI (speech-to-speech) webhooks. Paste the webhook signing key from the Developers page.

When to Use This Skill

  • How do I receive Hume EVI webhooks?
  • How do I verify the X-Hume-AI-Webhook-Signature header?
  • Why is my Hume webhook signature verification failing?
  • How do I handle chat_started, chat_ended and tool_call events?
  • How do I subscribe an EVI Config to webhook events?

Verification (core)

Hume signs each request with two headers:

X-Hume-AI-Webhook-Signature: <hex digest>
X-Hume-AI-Webhook-Timestamp: <unix epoch seconds>

The digest is HMAC-SHA256(signing key, "<raw body>.<timestamp>"), lowercase hex, sent bare with no prefix. Note the order: body first, then a period, then the timestamp. The key is the signing key's UTF-8 bytes, used as-is. Build the signed message as bytes: the raw body, a literal ., then the timestamp header exactly as received. Never decode and re-encode the body, and never JSON.parse / JSON.stringify before verifying.

const crypto = require('crypto');

function verifyHumeSignature(rawBody /* Buffer */, signatureHeader, timestampHeader, secret) {
  if (!secret || !signatureHeader || !timestampHeader) return false;

  // The whole header is one hex digest; validate format BEFORE decoding
  if (!/^[0-9a-fA-F]{64}$/.test(signatureHeader)) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(Buffer.concat([rawBody, Buffer.from('.'), Buffer.from(timestampHeader, 'latin1')]))
    .digest();
  const provided = Buffer.from(signatureHeader.toLowerCase(), 'hex'); // hex is case-insensitive
  return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
}

Missing or empty signature or timestamp header, malformed value, or mismatch: respond 401.

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

Hume's docs show manual verification code and no verification helper in an SDK, so the examples implement the documented algorithm directly. The signing key is the account's webhook signing key: Hume platform → Developers page → Generate signing key.

Timestamp tolerance is opt-in

Hume's own example rejects deliveries more than 180 seconds old. This skill does not by default: Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck applies no tolerance itself, so a fixed window would reject legitimate retries. Set HUME_WEBHOOK_TOLERANCE_SECONDS to opt in (X-Hume-AI-Webhook-Timestamp is Unix seconds; unset or 0 = off). Because the timestamp is signed, it cannot be swapped for a fresh one.

Event Types

Dispatch on the body's event_name field.

event_nameTriggered WhenKey fields
chat_startedA Chat session starts (new or resumed)chat_id, chat_group_id, config_id, start_time (ms), chat_start_type (new_chat_group or resumed_chat_group)
chat_endedA Chat session endsend_time (ms), duration_seconds, end_reason (e.g. USER_ENDED, USER_TIMEOUT, MAX_DURATION_TIMEOUT, INACTIVITY_TIMEOUT, ERROR)
tool_callThe model calls a tool during a Chattimestamp (ms), tool_call_message with name, parameters (stringified JSON), response_required, tool_call_id, tool_type (builtin or function)

Every payload also carries chat_group_id, chat_id, config_id, and optional caller_number (E.164, Twilio phone calls only) and custom_session_id (custom language models only). Timestamps inside payloads are milliseconds; the header timestamp is seconds.

Log and acknowledge any other event_name rather than failing.

Important Details

  • Method: POST. Hookdeck's Hume source manages the method as POST.
  • Subscribing: add a webhooks entry to an EVI Config: { "url": "...", "events": ["chat_started", "chat_ended", "tool_call"] }. Only subscribed events are sent.
  • Rotating the key: generating a new signing key immediately invalidates the old one. Only admins can do it in an organization.
  • tool_call replies: if response_required is true, send the tool response (or a tool error) back to EVI through the Control Plane API; the webhook response body is not used for it.
  • Retries: Hume's webhook docs do not describe a retry or timeout policy. Respond quickly and process asynchronously.
  • No delivery ID header. Build an idempotency key yourself, e.g. chat_id + event_name (+ tool_call_id for tool calls).

Environment Variables

HUME_WEBHOOK_SECRET=your_webhook_signing_key   # Developers page > Generate signing key
HUME_WEBHOOK_TOLERANCE_SECONDS=0               # optional; unset or 0 = no timestamp window (default)

Hume's docs call the variable HUME_WEBHOOK_SIGNING_KEY; this skill uses HUME_WEBHOOK_SECRET throughout.

Local Development

# Forward Hume webhooks to your local server (no account required)
npx hookdeck-cli listen 3000 hume --path /webhooks/hume

Put the resulting HTTPS URL in the webhooks array of your EVI Config, then start a Chat that uses that Config.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →