Gareth Wilson Gareth Wilson

Guide to Hume Webhooks: Features and Best Practices

Published


Hume webhooks report what happens in voice conversations with EVI, Hume AI's Empathic Voice Interface. When a chat starts, when it ends and why, and when EVI calls a tool, Hume posts the event to your server. Teams use them to log sessions, process transcripts once a call is over, track errors and timeouts, and run tool calls on their own backend.

This guide covers how to subscribe to EVI webhooks, the three events and their payloads, how deliveries are signed, and the practices that keep a Hume receiver reliable.

What are Hume webhooks?

Hume's webhooks belong to EVI, its speech-to-speech product. You subscribe in an EVI Config: the config's webhooks array lists URLs and the events each one should receive. Every chat that uses the config then sends those events to the URLs as JSON POST requests.

Each request carries X-Hume-AI-Webhook-Signature and X-Hume-AI-Webhook-Timestamp. The signature is a hex HMAC-SHA256 of the raw body, a full stop and the timestamp, in that order, keyed with your account's webhook signing key. Most providers sign the timestamp first. Hume puts the body first, and getting the order wrong is the most likely reason for a failed check.

Hume webhook features

FeatureDetails
ProductEVI (speech-to-speech)
Configurationwebhooks: [{ url, events }] on an EVI Config, through the API or SDKs
Eventschat_started, chat_ended, tool_call
PayloadFlat JSON with event_name and the chat identifiers
Signature headerX-Hume-AI-Webhook-Signature, bare hex
Timestamp headerX-Hume-AI-Webhook-Timestamp, Unix seconds
SchemeHMAC-SHA256, hex digest
Signed content{raw body}.{X-Hume-AI-Webhook-Timestamp}
Signing keyOne per account, generated on the Developers page
Replay guidanceHume's examples reject timestamps more than 180 seconds old

Hume doesn't document a retry policy, a response timeout, a delivery ID or source IP addresses for webhooks.

Common events

EventFires whenExtra fields
chat_startedA chat session starts, whether new or resumedstart_time, chat_start_type (new_chat_group or resumed_chat_group)
chat_endedA chat session endsend_time, duration_seconds, end_reason
tool_callEVI makes a tool call during a chattimestamp, tool_call_message

Every payload shares event_name, chat_group_id, chat_id and config_id, plus caller_number, which is only filled in for chats created through Hume's Twilio phone integration, and custom_session_id, which is only relevant with a custom language model. Here is Hume's chat_ended example:

{
  "event_name": "chat_ended",
  "chat_group_id": "9fc18597-3567-42d5-94d6-935bde84bf2f",
  "chat_id": "470a49f6-1dec-4afe-8b61-035d3b2d63b0",
  "config_id": "1b60e1a0-cc59-424a-8d2c-189d354db3f3",
  "caller_number": null,
  "custom_session_id": null,
  "end_time": 1716244958546,
  "duration_seconds": 180,
  "end_reason": "USER_ENDED"
}

end_reason values include USER_ENDED, USER_TIMEOUT, MAX_DURATION_TIMEOUT, INACTIVITY_TIMEOUT and ERROR. start_time, end_time and the tool call timestamp are milliseconds, while the signed header timestamp is seconds.

In a tool_call event, tool_call_message holds the tool name, its parameters as a JSON string, response_required, the tool_call_id and a tool_type of builtin or function. Parse parameters a second time to get an object. If response_required is true, send the tool response or tool error back to the chat through Hume's Control Plane API, not in the HTTP response to the webhook.

Setting up Hume webhooks

  1. Generate a webhook signing key. On the Developers page of the Hume platform, click Generate signing key, copy it and store it, for example as HUME_WEBHOOK_SECRET. For organisations, only admins can do this.
  2. Add a webhook to your EVI Config, listing your URL and events:
curl https://api.hume.ai/v0/evi/configs \
  -H "X-Hume-Api-Key: $HUME_API_KEY" \
  --json '{
    "evi_version": "3",
    "name": "Support agent",
    "webhooks": [{
      "url": "https://example.com/webhooks/hume",
      "events": ["chat_started", "chat_ended", "tool_call"]
    }]
  }'

The same webhooks field is available in Hume's TypeScript and Python SDKs when you create or update a config.

The signing key is per account and doesn't change when you rotate your API key. Generating a new one invalidates the old key immediately, and any payload signed with the old key then fails verification, so update every receiver at the same time.

If you use Hookdeck, create a source with the Hume source type and use its URL in the config's webhooks array. The source is for Hume EVI (speech-to-speech) webhooks: paste the webhook signing key from the Developers page into it. The source accepts POST only, which is what Hume sends.

Securing Hume webhooks

Read both headers. Build the signed content from the raw body, a . and the timestamp header exactly as received. Compute an HMAC-SHA256 keyed with the signing key as a string, encode it as hex and compare it with the signature header in constant time.

Hume's examples also reject requests whose timestamp is more than 180 seconds old. Below, that check is optional and off by default.

const crypto = require('crypto');
const express = require('express');

function verifyHumeSignature(rawBody, timestamp, signature, secret, toleranceSeconds = 0) {
  if (!timestamp || !signature || !secret) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false;

  // Optional replay check, off by default.
  if (toleranceSeconds > 0) {
    if (!/^\d+$/.test(timestamp)) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  }

  // Body first, then ".", then the timestamp.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .update('.')
    .update(timestamp, 'latin1')
    .digest('hex');

  const received = Buffer.from(signature.toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/hume', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyHumeSignature(
    req.body,
    req.get('x-hume-ai-webhook-timestamp'),
    req.get('x-hume-ai-webhook-signature'),
    process.env.HUME_WEBHOOK_SECRET
  );
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event_name. Acknowledge unknown events with a 200.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re
import time

HEX_64 = re.compile(r"[0-9a-fA-F]{64}")


def verify_hume_signature(raw_body: bytes, timestamp, signature, secret: str,
                          tolerance_seconds: int = 0) -> bool:
    if not timestamp or not signature or not secret:
        return False
    if not HEX_64.fullmatch(signature):
        return False
    # Optional replay check, off by default.
    if tolerance_seconds > 0:
        if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds:
            return False

    signed = raw_body + b"." + timestamp.encode("latin-1")  # body first
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_hume_signature(raw_body, request.headers.get("x-hume-ai-webhook-timestamp"),
#                       request.headers.get("x-hume-ai-webhook-signature"),
#                       os.environ["HUME_WEBHOOK_SECRET"])

Hume's own samples verify the body as a string. The versions above keep it as bytes, which avoids any change from decoding and re-encoding.

Hookdeck's Hume source type verifies HMAC-SHA256 over {body}.{X-Hume-AI-Webhook-Timestamp}, hex, keyed with the signing key, and applies no timestamp tolerance. A request that fails the check is marked as failed verification and is not delivered to your destinations.

Make Hume webhooks production-ready. Hookdeck Event Gateway verifies the X-Hume-AI-Webhook-Signature header on every EVI event, stores each chat and tool call event, and delivers it to your services with retries you control.

Hume webhook limitations and pain points

Body first, not timestamp first

The Problem: Verification fails on every request, though the key is right.

Why It Happens: Code adapted from Stripe-style verifiers signs timestamp.body. Hume signs body.timestamp. Both produce a valid-looking hex digest, so nothing points to the order as the cause.

Workarounds:

  • Build the signed content as raw body, ., timestamp.
  • Keep the body as bytes until after verification.

How Hookdeck Can Help: Hookdeck's Hume source builds the signed content in Hume's order, so your service only needs to trust the requests Hookdeck delivers.

Rotating the key breaks every receiver at once

The Problem: After someone generates a new signing key, all deliveries fail verification until every receiver is updated.

Why It Happens: There is one signing key per account and no overlap period. "If you generate a new signing key, the previous key is immediately invalidated."

Workarounds:

  • Rotate during a quiet period and update the key in every receiver straight away.
  • Keep the key in one secrets store that all receivers read.

How Hookdeck Can Help: With Hookdeck in front, the key lives in one Hookdeck source rather than in each service, so a rotation is a single change.

The 180-second window and redeliveries

The Problem: A delivery that you replay or that is retried later fails your own timestamp check.

Why It Happens: Hume's examples reject timestamps more than 180 seconds old. Hookdeck retries and replays resend the original headers, so a redelivery after three minutes carries an old timestamp.

Workarounds:

  • Leave the window off and deduplicate instead.
  • If you need a window, make it longer than the longest retry or replay delay you allow.

How Hookdeck Can Help: Hookdeck applies no timestamp tolerance, so redeliveries still pass verification at the source.

An undocumented delivery policy

The Problem: You don't know whether Hume retries a failed delivery, or how long it waits for your response.

Why It Happens: Hume's webhook documentation covers events and signing but not retries, timeouts or delivery IDs. Its sample handler also answers unknown events with a 400, which a sender may treat as a failure.

Workarounds:

  • Return 200 quickly for every verified event, including types you don't handle yet.
  • Reconcile important sessions through Hume's chat APIs.

How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives and stores it, then delivers to your service with retries you configure, whatever Hume's own policy turns out to be.

Best practices

Verify against the raw body

Read the body as bytes: express.raw({ type: '*/*' }) in Express, Buffer.from(await request.arrayBuffer()) in Next.js and await request.body() in FastAPI. Parse JSON only after the signature checks out.

Build your own idempotency key

Hume sends no event or delivery ID. Combine event_name with chat_id, and add tool_call_id for tool calls. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Processing a transcript or running a tool can take a while. Verify, store the event, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Keep the units straight

The header timestamp is in seconds. start_time, end_time and the tool call timestamp in the body are milliseconds, and duration_seconds is seconds.

Send tool results through the Control Plane

The webhook response doesn't carry tool output. When response_required is true, send a tool response or tool error to the chat through the Control Plane API, using the tool_call_id.

Conclusion

Hume signs EVI webhooks with a hex HMAC-SHA256 of the raw body, a full stop and the timestamp, keyed with one account-wide signing key. Sign in that order, verify against the raw bytes, build idempotency keys from the chat and tool call IDs, and plan key rotation, because a new key invalidates the old one at once.

Hookdeck Event Gateway verifies every Hume signature and holds each chat and tool call event until your service has processed it.

Get started with Hookdeck for free and handle Hume webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.