# Guide to Hume Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Product | EVI (speech-to-speech) |
| Configuration | `webhooks: [{ url, events }]` on an EVI Config, through the API or SDKs |
| Events | `chat_started`, `chat_ended`, `tool_call` |
| Payload | Flat JSON with `event_name` and the chat identifiers |
| Signature header | `X-Hume-AI-Webhook-Signature`, bare hex |
| Timestamp header | `X-Hume-AI-Webhook-Timestamp`, Unix seconds |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | `{raw body}.{X-Hume-AI-Webhook-Timestamp}` |
| Signing key | One per account, generated on the Developers page |
| Replay guidance | Hume'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

| Event | Fires when | Extra fields |
| --- | --- | --- |
| `chat_started` | A chat session starts, whether new or resumed | `start_time`, `chat_start_type` (`new_chat_group` or `resumed_chat_group`) |
| `chat_ended` | A chat session ends | `end_time`, `duration_seconds`, `end_reason` |
| `tool_call` | EVI makes a tool call during a chat | `timestamp`, `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:

```json
{
  "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](https://app.hume.ai/settings/keys) 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:

```bash
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.

```javascript
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:

```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](/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](/docs/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](https://hookdeck.com) verifies every Hume signature and holds each chat and tool call event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Hume webhooks reliably in minutes.