> This page is for Voice APIs.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://dev.hume.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://dev.hume.ai/_mcp/server.

# Webhooks

EVI webhooks send structured payloads to your specified URL in real time, allowing your application to respond to
key events during EVI **Chat** sessions. They enable you to connect EVI with your systems to monitor events, automate
workflows, and gain valuable insights into user interactions.

**Looking for example code?** See example projects in TypeScript and Python on GitHub:

#### [TypeScript Example](https://github.com/HumeAI/hume-api-examples/blob/main/evi/evi-typescript-webhooks/README.md)

See EVI WebHooks implemented in TypeScript.

#### [Python Example](https://github.com/HumeAI/hume-api-examples/blob/main/evi/evi-python-webhooks/README.md)

See EVI WebHooks implemented in Python.

## Supported events

The following section details each supported event, including what triggers the event, the structure of its payload,
and practical use cases to help you integrate it into your workflows.

### Chat started

#### Trigger

The `chat_started` event is triggered when a new **Chat** session is started. This includes both new and resumed
sessions.

#### Use cases

* **Workflow initiation**: Use this event to trigger workflows such as starting a logging session, updating a
  dashboard, or notifying a team.
* **Activity monitoring**: Track when new or resumed sessions occur to measure usage trends or generate
  real-time analytics.
* **Custom integrations**: Push session start data to third-party systems (e.g., Zapier) to automate
  downstream actions like data collection or tracking.

#### Payload structure

<table>
  <tbody>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>

    <tr>
      <td>
        `event_name`
      </td>

      <td>
        `string`
      </td>

      <td>
        Always `"chat_started"`.
      </td>
    </tr>

    <tr>
      <td>
        `chat_group_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat Group** associated with the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `chat_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `config_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the EVI **Config** used for the session.
      </td>
    </tr>

    <tr>
      <td>
        `caller_number`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* Phone number of the caller in E.164 format (e.g., `+12223333333`). This field is included only if
        the Chat was created via the [Twilio phone calling](/docs/integrations/twilio)
        integration.
      </td>
    </tr>

    <tr>
      <td>
        `custom_session_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* User-defined session ID. Relevant only when employing a [custom language model](/docs/speech-to-speech-evi/guides/custom-language-model) in the EVI Config.
      </td>
    </tr>

    <tr>
      <td>
        `start_time`
      </td>

      <td>
        `integer`
      </td>

      <td>
        Numeric	Unix timestamp (in milliseconds) indicating when the session started.
      </td>
    </tr>

    <tr>
      <td>
        `chat_start_type`
      </td>

      <td>
        `string`
      </td>

      <td>
        Indicates if the session is new (`"new_chat_group"`) or resumed (`"resumed_chat_group"`).
      </td>
    </tr>
  </tbody>
</table>

#### Sample payload

#### Sample payload

```json
{
  "event_name": "chat_started",
  "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,
  "start_time": 1716244940648,
  "chat_start_type": "new_chat_group"
}
```

### Chat ended

#### Trigger

The `chat_ended` event is triggered when a **Chat** session is ended.

#### Use cases

* **Analytics**: Measure session durations and analyze reasons for chat termination to improve performance or
  user experience.
* **Workflow automations**: Automatically process transcripts or save session data to external systems for
  further analysis or reporting.
* **Error monitoring**: Track sessions that terminate with an error or timeout to identify and address
  recurring issues.

#### Payload structure

<table>
  <tbody>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>

    <tr>
      <td>
        `event_name`
      </td>

      <td>
        `string`
      </td>

      <td>
        Always `"chat_ended"`.
      </td>
    </tr>

    <tr>
      <td>
        `chat_group_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat Group** associated with the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `chat_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `config_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the EVI **Config** used for the session.
      </td>
    </tr>

    <tr>
      <td>
        `caller_number`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* Phone number of the caller in E.164 format (e.g., `+12223333333`). This field is included only if
        the Chat was created via the [Twilio phone calling](/docs/integrations/twilio)
        integration.
      </td>
    </tr>

    <tr>
      <td>
        `custom_session_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* User-defined session ID. Relevant only when employing a [custom language model](/docs/speech-to-speech-evi/guides/custom-language-model) in the EVI Config.
      </td>
    </tr>

    <tr>
      <td>
        `end_time`
      </td>

      <td>
        `integer`
      </td>

      <td>
        Numeric Unix timestamp (in milliseconds) indicating when the session ended.
      </td>
    </tr>

    <tr>
      <td>
        `duration_seconds`
      </td>

      <td>
        `integer`
      </td>

      <td>
        Total duration of the session in seconds.
      </td>
    </tr>

    <tr>
      <td>
        `end_reason`
      </td>

      <td>
        `string`
      </td>

      <td>
        Reason for the session's termination (e.g., `USER_ENDED`, `USER_TIMEOUT`, `MAX_DURATION_TIMEOUT`,
        `INACTIVITY_TIMEOUT`, or `ERROR`.).
      </td>
    </tr>
  </tbody>
</table>

#### Sample payload

#### Sample payload

```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"
}
```

### Tool call

#### Trigger

The `tool_call` event is triggered when a tool call is made during a **Chat** session.

#### Use cases

* **Server-side tool use**: Invoke your tools server-side and send tool responses to EVI via the [Control Plane API](/docs/speech-to-speech-evi/guides/control-plane).
* **Analytics**: Track tool calls to measure usage patterns and identify popular tools.
* **Error monitoring**: Track tool calls that fail to complete to identify and address recurring issues.

#### Payload structure

<table>
  <tbody>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>

    <tr>
      <td>
        `event_name`
      </td>

      <td>
        `string`
      </td>

      <td>
        Always `"tool_call"`.
      </td>
    </tr>

    <tr>
      <td>
        `chat_group_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat Group** associated with the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `chat_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the **Chat** session.
      </td>
    </tr>

    <tr>
      <td>
        `config_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        Unique ID of the EVI **Config** used for the session.
      </td>
    </tr>

    <tr>
      <td>
        `caller_number`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* Phone number of the caller in E.164 format (e.g., `+12223333333`). This field is included only if
        the Chat was created via the [Twilio phone calling](/docs/integrations/twilio)
        integration.
      </td>
    </tr>

    <tr>
      <td>
        `custom_session_id`
      </td>

      <td>
        `string`
      </td>

      <td>
        *(Optional)* User-defined session ID. Relevant only when employing a [custom language model](/docs/speech-to-speech-evi/guides/custom-language-model) in the EVI Config.
      </td>
    </tr>

    <tr>
      <td>
        `timestamp`
      </td>

      <td>
        `integer`
      </td>

      <td>
        Numeric Unix timestamp (in milliseconds) indicating when the tool call message was sent.
      </td>
    </tr>

    <tr>
      <td>
        `tool_call_message[name]`
      </td>

      <td>
        `string`
      </td>

      <td>
        Name of the tool call's corresponding tool.
      </td>
    </tr>

    <tr>
      <td>
        `tool_call_message[parameters]`
      </td>

      <td>
        `string`
      </td>

      <td>
        Parameters of the tool call. Is a stringified JSON schema.
      </td>
    </tr>

    <tr>
      <td>
        `tool_call_message[response_required]`
      </td>

      <td>
        `boolean`
      </td>

      <td>
        Indicates whether a response to the tool call is required from the developer, either in the form of a Tool Response message or a Tool Error message.
      </td>
    </tr>

    <tr>
      <td>
        `tool_call_message[tool_call_id]`
      </td>

      <td>
        `string`
      </td>

      <td>
        The unique identifier for the specific tool call instance.
      </td>
    </tr>

    <tr>
      <td>
        `tool_call_message[tool_type]`
      </td>

      <td>
        `enum`
      </td>

      <td>
        Type of tool called. Either `builtin` for natively implemented tools, like web search, or `function` for user-defined tools.
      </td>
    </tr>
  </tbody>
</table>

#### Sample payload

#### Sample payload

```json
{
  "event_name": "tool_call",
  "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": "string",
  "timestamp": 1716244958546,
  "tool_call_message": {
    "name": "get_current_weather",
    "parameters": "{\"format\": \"fahrenheit\", \"location\": \"San Francisco, CA\"}",
    "response_required": true,
    "tool_call_id": "d20827af-5d8d-4f66-b6b9-ce2e3e1ea2b2",
    "tool_type": "function"
  }
}
```

## Subscribing to events

To receive event notifications, define your webhook URL and specify the events you want to subscribe to within your
[EVI Config](/reference/speech-to-speech-evi/configs/create-config#request.body.webhooks). The example below
demonstrates how to configure a webhook URL for the `chat_started` and `chat_ended` events:

#### cURL

```cURL maxLines=0
curl https://api.hume.ai/v0/evi/configs \
  -H "X-Hume-Api-Key: <YOUR_API_KEY>" \
  --json '{
    "evi_version": "3",
    "name": "Sample Webhook Config",
    "webhooks": [{
      "url": <YOUR_WEBHOOK_URL>,
      "events": ["chat_started", "chat_ended", "tool_call"]
    }]
  }'
```

#### TypeScript

```typescript maxLines=0
import { HumeClient, Hume } from "hume";

const client = new HumeClient({ apiKey: "<YOUR_API_KEY>" });
await client.empathicVoice.configs.createConfig({
  "evi_version": 3,
  "name": "Sample Webhook Config",
  "webhooks": [ 
    { 
      "url": "<YOUR_WEBHOOK_URL>", 
      "events": ["chat_started", "chat_ended", "tool_call"]
    }
  ]
});
```

#### Python

```python maxLines=0
from hume import HumeClient

client = HumeClient(api_key="YOUR_API_KEY")
client.empathic_voice.configs.create_config({ 
  "evi_version": 3, 
  "name": "Sample Webhook Config",
  "webhooks": [ 
    { 
      "url": "<YOUR_WEBHOOK_URL>", 
      "events": ["chat_started", "chat_ended", "tool_call"] 
    } 
  ]
})
```

## Handling events

When EVI sends event payloads to your webhook URL, your application can process them by implementing a handler. Below
are simplified example implementations in TypeScript and Python for handling `chat_started` and `chat_ended` events.

#### TypeScript

```typescript maxLines=0
import type { WebhookEvent } from "hume/serialization/resources/empathicVoice/types/WebhookEvent";

// Route to handle webhook events
app.post("/hume-webhook", (req: Request, res: Response) => {
  // Validate and parse using WebhookEvent
  const event = WebhookEvent.parseOrThrow(JSON.parse(req.body));

  try {
    // Handle the specific event type
    switch (event.eventName) {
      case 'chat_started':
        console.info('Processing chat_started event:', event);
        // Add additional chat_started processing logic here
        break;
      
      case 'chat_ended':
        console.info("Processing chat_ended event:", event);
        // Add additional chat_ended processing logic here
        break;

      case 'tool_call':
        console.info("Processing tool_call event:", event);
        // Add additional tool_call processing logic here
        break;

      default:
        res.status(400).json({ 
          error: `Unsupported event type: '${event.eventName}'` 
        });
        return;
    }

    res.json({ 
      status: "success", 
      message: `${event.event_name} processed` 
    });
  } catch (error) {
    console.error("Error processing event:", error);
    res.status(500).json({ error: "Internal server error" });
  }
});
```

#### Python

```python maxLines=0
from hume.empathic_voice.types import (
    WebhookEvent,
    WebhookEventChatStarted,
    WebhookEventChatEnded,
    WebhookEventToolCall
)

@app.post("/hume-webhook")
async def webhook_handler(event: WebhookEvent):
    """
    Handles incoming webhook events.
    """
    try:
        if isinstance(event, WebhookEventChatStarted):
            print(f"Processing chat_started event: {event.dict()}")
            # Add your logic for handling chat_started events here

        elif isinstance(event, WebhookEventChatEnded):
            print(f"Processing chat_ended event: {event.dict()}")
            # Add your logic for handling chat_ended events here
        
        elif isinstance(event, WebhookEventToolCall):
            print(f"Processing tool_call event: {event.dict()}")
            # Add your logic for handling tool_call events here
      
        else:
            raise HTTPException(
                status_code=400, 
                detail=f"Unsupported event type: '{event.event_name}'"
            )

        # Respond with a success message
        return {
            "status": "success", 
            "message": f"{event.event_name} processed"
        }

    except Exception as e:
        print(f"Error in webhook handler: {e}")
        raise HTTPException(
            status_code=500, 
            detail=f"Internal server error: {e}"
        )
```

### Security

To ensure the authenticity and integrity of webhook payloads, EVI includes an HMAC signature and a timestamp
in each request. Implementing verification safeguards your application from tampering and replay attacks.

#### Webhook signing key

Each webhook payload is signed with a dedicated webhook signing key. The signing key is a stable, per-account secret
that does not change when you rotate your API key or switch authentication methods.

To provision your webhook signing key:

1. Navigate to the [Developers page](https://app.hume.ai/settings/keys) on the Hume platform.
2. Click **Generate signing key**.
3. Copy the key and store it securely. You will use this key to verify incoming webhook payloads.

> **Info**
>
> **If you generate a new signing key, the previous key is immediately invalidated.** Any webhook payloads signed with
> the old key will fail verification. For organizations, only admins can generate and rotate the webhook signing key.

#### Verifying authenticity

Each webhook request contains the following headers:

* `X-Hume-AI-Webhook-Signature`: HMAC-SHA256 signature of the payload and timestamp, signed using your webhook signing key.
* `X-Hume-AI-Webhook-Timestamp`: Unix timestamp indicating when the request was sent.

To verify authenticity:

1. Retrieve the `X-Hume-AI-Webhook-Signature` and `X-Hume-AI-Webhook-Timestamp` headers.
2. Concatenate the payload and timestamp, then compute the HMAC-SHA256 hash using your webhook signing key.
3. Compare the computed hash with the provided signature using a timing-safe comparison.

#### Preventing replay attacks

Validate the `X-Hume-AI-Webhook-Timestamp` header to ensure the request is recent:

1. Check if the timestamp is within a predefined range (e.g., 3 minutes from the current time).
2. Reject requests with timestamps outside this range.

#### Example

The following example combines both signature verification and timestamp validation into a single function:

#### TypeScript

```typescript maxLines=0
import * as crypto from 'crypto';
import { IncomingHttpHeaders } from 'http';

export function validateWebhookHeaders(
  payload: string,
  headers: IncomingHttpHeaders,
): void {
  // Extract required headers
  const timestamp = headers['x-hume-ai-webhook-timestamp'] as string;
  if (!timestamp) {
    throw new Error('Missing timestamp header');
  }

  const signature = headers['x-hume-ai-webhook-signature'] as string;
  if (!signature) {
    throw new Error('Missing signature header');
  }

  // Validate HMAC signature using the webhook signing key
  const signingKey = process.env.HUME_WEBHOOK_SIGNING_KEY;
  if (!signingKey) {
    throw new Error('HUME_WEBHOOK_SIGNING_KEY is not set in environment variables');
  }

  const message = `${payload}.${timestamp}`;
  const expectedSig = crypto
    .createHmac('sha256', signingKey)
    .update(message)
    .digest('hex');

  const signatureBuffer = Buffer.from(signature, 'utf8');
  const expectedSigBuffer = Buffer.from(expectedSig, 'utf8');
  const validSignature =
    signatureBuffer.length === expectedSigBuffer.length &&
    crypto.timingSafeEqual(signatureBuffer, expectedSigBuffer);

  if (!validSignature) {
    throw new Error('Invalid HMAC signature');
  }

  // Validate timestamp to prevent replay attacks
  const timestampInt = parseInt(timestamp, 10);
  if (isNaN(timestampInt)) {
    throw new Error('Invalid timestamp format');
  }

  const currentTime = Math.floor(Date.now() / 1000);
  const TIMESTAMP_VALIDATION_WINDOW = 180;
  if (currentTime - timestampInt > TIMESTAMP_VALIDATION_WINDOW) {
    throw new Error('The timestamp on the request is too old');
  }
}
```

#### Python

```python maxLines=0
import hashlib
import hmac
import os
import time
from starlette.datastructures import Headers

def validate_webhook_headers(payload: str, headers: Headers) -> None:
    timestamp = headers.get("X-Hume-AI-Webhook-Timestamp")
    signature = headers.get("X-Hume-AI-Webhook-Signature")

    if not signature:
        raise ValueError("Missing HMAC signature")

    if not timestamp:
        raise ValueError("Missing timestamp")

    # Validate HMAC signature using the webhook signing key
    signing_key = os.environ.get("HUME_WEBHOOK_SIGNING_KEY")
    if not signing_key:
        raise ValueError("HUME_WEBHOOK_SIGNING_KEY is not set in environment variables")

    message = (payload + "." + timestamp).encode("utf-8")
    expected_sig = hmac.new(
        key=signing_key.encode("utf-8"),
        msg=message,
        digestmod=hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_sig):
        raise ValueError("Invalid HMAC signature")

    # Validate timestamp to prevent replay attacks
    try:
        timestamp_int = int(timestamp)
    except ValueError:
        raise ValueError("Invalid timestamp format")

    current_time = int(time.time())
    TIMESTAMP_VALIDATION_WINDOW = 180
    if current_time - timestamp_int > TIMESTAMP_VALIDATION_WINDOW:
        raise ValueError("The timestamp on the request is too old")
```

---