> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# 通話後Webhook

## 概要

通話後[Webhook](/docs/ja/eleven-api/resources/webhooks)を使用すると、分析完了後に通話に関する詳細情報を受け取れます。有効にすると、ElevenLabsは包括的な通話データを含むPOSTリクエストを指定したエンドポイントに送信します。

ElevenLabsは、次の3種類の通話後Webhookに対応しています。

* **文字起こしWebhook**（`post_call_transcription`）：文字起こし、分析結果、メタデータを含む完全な会話データ
* **オーディオWebhook**（`post_call_audio`）：会話全体のbase64エンコードされたオーディオを含む最小限のデータ
* **通話開始失敗Webhook**（`call_initiation_failure`）：失敗理由やメタデータを含む、通話開始に失敗した試行に関する情報

## 通話後Webhookを有効にする

通話後Webhookは、ElevenAgentsの[設定ページ](https://elevenlabs.io/app/agents/settings)からワークスペース内のすべてのエージェントに対して有効にできます。

![通話後Webhook設定](/docs/_fern-img/eb5d768612d6461a21bc3127611f60724be3e1a55005af43faf48f2d7bf23807.webp)

> **Warning**
>
> 通話後Webhookは、成功とみなされるために200ステータスコードを返す必要があります。連続して10回以上失敗し、最後の正常な配信から7日以上経過している場合、または正常に配信されたことが一度もない場合、繰り返し失敗するWebhookは自動的に無効になります。

> **Note**
>
> 通話後Webhookは失敗時に自動で再試行できます。[Webhookの再試行](/docs/ja/eleven-api/resources/webhooks#retries)を参照してください。

### 認証

受信側は、すべての受信Webhookを検証することが重要です。Webhookは現在、HMAC署名による認証に対応しています。HMAC認証は次の手順で設定します。

* Webhookの作成時に生成された共有シークレットを安全に保存する
* SDKを使用してエンドポイントでElevenLabs-Signatureヘッダーを検証する

JavaScript SDKでは`constructEvent`を、Python SDKでは\*\*`rawBody`**、**`sig_header`**、**`secret`\*\*を指定する`construct_event`を利用できます（Pythonでは`payload`／`signature`という名前ではありません）。どちらも署名の検証、タイムスタンプの検証、JSONペイロードの解析を行います。

#### Python

FastAPIを使用したWebhookハンドラーの例：

```python
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from elevenlabs.client import ElevenLabs
from elevenlabs.errors import BadRequestError
import os

load_dotenv()

app = FastAPI()
elevenlabs = ElevenLabs(
    api_key=os.getenv("ELEVENLABS_API_KEY"),
)

WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")

@app.post("/webhook")
async def receive_message(request: Request):
    payload = await request.body()
    signature = request.headers.get("elevenlabs-signature")

    try:
        event = elevenlabs.webhooks.construct_event(
            rawBody=payload.decode("utf-8"),
            sig_header=signature,
            secret=WEBHOOK_SECRET,
        )
    except BadRequestError as e:
        return JSONResponse(content={"error": "Invalid signature"}, status_code=401)

    # construct_event returns a dict (parsed JSON), not an object with attributes
    if event.get("type") == "post_call_transcription":
        print(f"Received transcription: {event.get('data')}")

    return {"status": "received"}
```

#### JavaScript

#### Express

Expressを使用したWebhookハンドラーの例：

```javascript
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
import express from 'express';

const app = express();

const elevenlabs = new ElevenLabsClient();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Use express.text() to preserve raw body for signature verification
app.post('/webhook', express.text({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['elevenlabs-signature'];
  const payload = req.body; // Raw string body

  let event;
  try {
    event = await elevenlabs.webhooks.constructEvent(payload, signature, WEBHOOK_SECRET);
  } catch (error) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the webhook event
  if (event.type === 'post_call_transcription') {
    console.log('Received transcription:', event.data);
  }

  res.status(200).json({ received: true });
});
```

#### Next.js

Next.js APIルートを使用したWebhookハンドラーの例：

**`app/api/webhook/route.ts`**

```typescript app/api/webhook/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';

const elevenlabs = new ElevenLabsClient();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get('elevenlabs-signature');

  let event;
  try {
    event = await elevenlabs.webhooks.constructEvent(body, signature, WEBHOOK_SECRET);
  } catch (error) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  // Process the webhook event
  if (event.type === 'post_call_transcription') {
    console.log('Received transcription:', event.data);
  }

  return NextResponse.json({ received: true }, { status: 200 });
}
```

### IP許可リスト

セキュリティを強化するために、ElevenLabsの静的送信IPを許可リストに追加できます。IPアドレスの完全な一覧は、[IP許可リスト](/docs/ja/eleven-api/resources/ip-allowlisting)を参照してください。

> **Tip**
>
> IP許可リストとHMAC署名検証を組み合わせることで、多層的なセキュリティを実現できます。

## Webhookレスポンスの構造

ElevenLabsは、データ構造がそれぞれ異なる3種類の通話後Webhookを送信します：

### 文字起こしWebhook（`post_call_transcription`）

完全な文字起こし、分析結果、メタデータを含む包括的な会話データです。

#### トップレベルのフィールド

| フィールド             | 型      | 説明                                           |
| ----------------- | ------ | -------------------------------------------- |
| `type`            | string | イベントの種類（常に`post_call_transcription`）         |
| `data`            | object | `ConversationHistoryCommonModel`構造を使用した会話データ |
| `event_timestamp` | number | このイベントが発生したUTCのUnix時刻                        |

#### データオブジェクトの構造

`data`オブジェクトには以下が含まれます：

| フィールド                                 | 型       | 説明                                   |
| ------------------------------------- | ------- | ------------------------------------ |
| `agent_id`                            | string  | 通話を処理したエージェントのID                     |
| `agent_name`                          | string  | 会話時点でのエージェント名                        |
| `conversation_id`                     | string  | 会話の一意の識別子                            |
| `status`                              | string  | 会話のステータス（例："done"）                   |
| `user_id`                             | string  | 利用可能な場合のユーザー識別子                      |
| `branch_id`                           | string  | 該当する場合、会話で使用されたエージェントブランチ            |
| `version_id`                          | string  | 通話中にアクティブだったエージェントバージョン（スナップショット）のID |
| `environment`                         | string  | 環境変数の解決に使用した環境                       |
| `transcript`                          | array   | ターンを含む完全な会話の文字起こし                    |
| `metadata`                            | object  | 通話のタイミング、コスト、電話の詳細                   |
| `analysis`                            | object  | 評価結果と会話の要約                           |
| `conversation_initiation_client_data` | object  | 設定の上書きと動的変数                          |
| `has_audio`                           | boolean | 会話で利用可能なオーディオがあるかどうか                 |
| `has_user_audio`                      | boolean | 会話でユーザーのオーディオを利用できるかどうか              |
| `has_response_audio`                  | boolean | 会話でエージェントの応答オーディオを利用できるかどうか          |

### オーディオWebhook（`post_call_audio`）

会話全体のオーディオをBase64エンコードしたMP3を含む、最小限のデータです。

#### トップレベルのフィールド

| フィールド             | 型      | 説明                           |
| ----------------- | ------ | ---------------------------- |
| `type`            | string | イベントの種類（常に`post_call_audio`） |
| `data`            | object | 最小限のオーディオデータ                 |
| `event_timestamp` | number | このイベントが発生したUTCのUnix時刻        |

#### データオブジェクトの構造

`data`オブジェクトには次の項目のみが含まれます：

| フィールド             | 型      | 説明                                |
| ----------------- | ------ | --------------------------------- |
| `agent_id`        | string | 通話を処理したエージェントのID                  |
| `conversation_id` | string | 会話の一意の識別子                         |
| `full_audio`      | string | MP3形式の会話全体のオーディオを含むBase64エンコード文字列 |

> **Warning**
>
> オーディオWebhookには、上記の3つのフィールドのみが含まれます。文字起こしデータ、
> メタデータ、分析結果、その他の会話の詳細は**含まれません**。

### 通話開始失敗Webhook（`call_initiation_failure`）

失敗の理由や通信プロバイダーのメタデータなど、電話通話の開始試行に関する情報が含まれます。

> **Note**
>
> 通話開始失敗Webhookイベントは、接続エラー、ユーザーによる通話の拒否、またはユーザーが通話に応答しなかったために
> 通話を開始できなかった場合に送信されます。通話が留守番電話につながった場合、または自動サービスが
> 応答した場合は、通話は正常に開始されているため、通話開始失敗Webhookは送信されません。

#### トップレベルのフィールド

| フィールド             | 型      | 説明                                   |
| ----------------- | ------ | ------------------------------------ |
| `type`            | string | イベントの種類（常に`call_initiation_failure`） |
| `data`            | object | 通話開始失敗データ                            |
| `event_timestamp` | number | このイベントが発生したUTCのUnix時刻                |

#### データオブジェクトの構造

`data`オブジェクトには以下が含まれます：

| フィールド             | 型      | 説明                                 |
| ----------------- | ------ | ---------------------------------- |
| `agent_id`        | string | 通話の処理に割り当てられたエージェントのID             |
| `conversation_id` | string | 会話の一意の識別子                          |
| `failure_reason`  | string | 失敗理由（"busy"、"no-answer"、"unknown"） |
| `metadata`        | object | 通信プロバイダーから提供される追加データ               |

#### メタデータオブジェクトの構造

`metadata`オブジェクトの構造は、発信通話がTwilio経由かSIPトランキング経由かによって異なります。このオブジェクトには、両者を区別する`type`フィールドと、プロバイダー固有の詳細を含む`body`フィールドがあります。

**SIPメタデータ**（`type: "sip"`）：

| フィールド  | 型      | 必須 | 説明                 |
| ------ | ------ | -- | ------------------ |
| `type` | string | はい | プロバイダーの種類（常に`sip`） |
| `body` | object | はい | SIP固有の通話失敗情報       |

SIPメタデータの`body`オブジェクトには以下が含まれます：

| フィールド             | 型      | 必須  | 説明                                                                                   |
| ----------------- | ------ | --- | ------------------------------------------------------------------------------------ |
| `from_number`     | number | はい  | 通話を開始した相手の電話番号                                                                       |
| `to_number`       | number | はい  | 着信側の電話番号                                                                             |
| `sip_status_code` | number | はい  | SIPレスポンスのステータスコード（例：話し中の場合は486）                                                      |
| `error_reason`    | string | はい  | 人が読み取れるエラーの説明                                                                        |
| `call_sid`        | string | はい  | SIP通話セッション識別子                                                                        |
| `twirp_code`      | string | いいえ | 該当する場合の[Twirpエラーコード](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes) |
| `sip_status`      | string | いいえ | ステータスコードに対応するSIPステータステキスト                                                            |

**Twilioメタデータ**（`type: "twilio"`）：

| フィールド  | 型      | 必須 | 説明                                                                                                            |
| ------ | ------ | -- | ------------------------------------------------------------------------------------------------------------- |
| `type` | string | はい | プロバイダーの種類（常に`twilio`）                                                                                         |
| `body` | object | はい | 通話の詳細を含むTwilio StatusCallbackの本文。詳細は[こちら](https://www.twilio.com/docs/voice/api/call-resource#statuscallback) |

## Webhookペイロードの例

### 文字起こしWebhookの例

```json
{
  "type": "post_call_transcription",
  "event_timestamp": 1739537297,
  "data": {
    "agent_id": "xyz",
    "conversation_id": "abc",
    "status": "done",
    "user_id": "user123",
    "transcript": [
      {
        "role": "agent",
        "message": "Hey there angelo. How are you?",
        "tool_calls": null,
        "tool_results": null,
        "feedback": null,
        "time_in_call_secs": 0,
        "conversation_turn_metrics": null
      },
      {
        "role": "user",
        "message": "Hey, can you tell me, like, a fun fact about 11 Labs?",
        "tool_calls": null,
        "tool_results": null,
        "feedback": null,
        "time_in_call_secs": 2,
        "conversation_turn_metrics": null
      },
      {
        "role": "agent",
        "message": "I do not have access to fun facts about Eleven Labs. However, I can share some general information about the company. Eleven Labs is an AI voice technology platform that specializes in voice cloning and text-to-speech...",
        "tool_calls": null,
        "tool_results": null,
        "feedback": null,
        "time_in_call_secs": 9,
        "conversation_turn_metrics": {
          "convai_llm_service_ttfb": {
            "elapsed_time": 0.3704247010173276
          },
          "convai_llm_service_ttf_sentence": {
            "elapsed_time": 0.5551181449554861
          }
        }
      }
    ],
    "metadata": {
      "start_time_unix_secs": 1739537297,
      "call_duration_secs": 22,
      "cost": 296,
      "deletion_settings": {
        "deletion_time_unix_secs": 1802609320,
        "deleted_logs_at_time_unix_secs": null,
        "deleted_audio_at_time_unix_secs": null,
        "deleted_transcript_at_time_unix_secs": null,
        "delete_transcript_and_pii": true,
        "delete_audio": true
      },
      "feedback": {
        "overall_score": null,
        "likes": 0,
        "dislikes": 0
      },
      "authorization_method": "authorization_header",
      "charging": {
        "dev_discount": true
      },
      "termination_reason": ""
    },
    "analysis": {
      "evaluation_criteria_results": {},
      "data_collection_results": {},
      "call_successful": "success",
      "transcript_summary": "The conversation begins with the agent asking how Angelo is, but Angelo redirects the conversation by requesting a fun fact about 11 Labs. The agent acknowledges they don't have specific fun facts about Eleven Labs but offers to provide general information about the company. They briefly describe Eleven Labs as an AI voice technology platform specializing in voice cloning and text-to-speech technology. The conversation is brief and informational, with the agent adapting to the user's request despite not having the exact information asked for."
    },
    "conversation_initiation_client_data": {
      "conversation_config_override": {
        "agent": {
          "prompt": null,
          "first_message": null,
          "language": "en"
        },
        "tts": {
          "voice_id": null
        }
      },
      "custom_llm_extra_body": {},
      "dynamic_variables": {
        "user_name": "angelo"
      },
      "branch_id": null,
      "environment": null
    }
  }
}
```

### オーディオWebhookの例

```json
{
  "type": "post_call_audio",
  "event_timestamp": 1739537319,
  "data": {
    "agent_id": "xyz",
    "conversation_id": "abc",
    "full_audio": "SUQzBAAAAAAA...base64_encoded_mp3_data...AAAAAAAAAA=="
  }
}
```

### 通話開始失敗Webhookの例

#### Twilioメタデータの例

```json
{
  "type": "call_initiation_failure",
  "event_timestamp": 1759931652,
  "data": {
    "agent_id": "xyz",
    "conversation_id": "abc",
    "failure_reason": "busy",
    "metadata": {
      "type": "twilio",
      "body": {
        "Called": "+441111111111",
        "ToState": "",
        "CallerCountry": "US",
        "Direction": "outbound-api",
        "Timestamp": "Wed, 08 Oct 2025 13:54:12 +0000",
        "CallbackSource": "call-progress-events",
        "SipResponseCode": "487",
        "CallerState": "WA",
        "ToZip": "",
        "SequenceNumber": "2",
        "CallSid": "CA8367245817625617832576245724",
        "To": "+441111111111",
        "CallerZip": "98631",
        "ToCountry": "GB",
        "CalledZip": "",
        "ApiVersion": "2010-04-01",
        "CalledCity": "",
        "CallStatus": "busy",
        "Duration": "0",
        "From": "+11111111111",
        "CallDuration": "0",
        "AccountSid": "AC37682153267845716245762454a",
        "CalledCountry": "GB",
        "CallerCity": "RAYMOND",
        "ToCity": "",
        "FromCountry": "US",
        "Caller": "+11111111111",
        "FromCity": "RAYMOND",
        "CalledState": "",
        "FromZip": "12345",
        "FromState": "WA"
      }
    }
  }
}
```

#### SIPメタデータの例

```json
{
  "type": "call_initiation_failure",
  "event_timestamp": 1759931652,
  "data": {
    "agent_id": "xyz",
    "conversation_id": "abc",
    "failure_reason": "busy",
    "metadata": {
      "type": "sip",
      "body": {
        "from_number": "+441111111111",
        "to_number": "+11111111111",
        "sip_status_code": 486,
        "error_reason": "INVITE failed: sip status: 486: Busy here (SIP 486)",
        "call_sid": "d8e7f6a5-b4c3-4d5e-8f9a-0b1c2d3e4f5a",
        "sip_status": "Busy here",
        "twirp_code": "unavailable"
      }
    }
  }
}
```

## オーディオWebhookの配信

オーディオWebhookは文字起こしWebhookとは別に配信され、会話を識別するために必要な基本フィールドと、Base64エンコードされたオーディオデータのみを含みます。

> **Note**
>
> オーディオWebhookは、Webhook設定の「オーディオデータを送信」トグルで有効または無効にできます。
> この設定は、ワークスペースレベル（ElevenAgents設定内）と、エージェントレベル（個々のエージェントのWebhook上書き設定内）の
> 両方で構成できます。

### ストリーミング配信

大きなオーディオファイルを効率的に処理するため、オーディオWebhookは`transfer-encoding: chunked`ヘッダーを使用するストリーミングHTTPリクエストとして配信されます。各リクエストは5分後にタイムアウトします。

### 再試行

Webhookで再試行を有効にすると、失敗したオーディオ配信は文字起こしWebhookと同じスケジュールで再試行されます。再試行では完全なオーディオペイロードが再送されるため、`conversation_id`で重複を除外してください。スケジュール、再試行可能なエラー、オーディオサイズの上限については、[Webhookの再試行](/docs/ja/eleven-api/resources/webhooks#retries)を参照してください。

### オーディオWebhookの処理

オーディオWebhookはチャンク転送エンコーディングで配信されるため、ストリーミングデータを適切に処理する必要があります：

```python

import base64
import json
from aiohttp import web

async def handle_webhook(request):

    # Check if this is a chunked/streaming request
    if request.headers.get("transfer-encoding", "").lower() == "chunked":
        # Read streaming data in chunks
        chunked_body = bytearray()
        while True:
            chunk = await request.content.read(8192)  # 8KB chunks
            if not chunk:
                break
            chunked_body.extend(chunk)

        # Parse the complete payload
        request_body = json.loads(chunked_body.decode("utf-8"))
    else:
        # Handle regular requests
        body_bytes = await request.read()
        request_body = json.loads(body_bytes.decode('utf-8'))

    # Process different webhook types
    if request_body["type"] == "post_call_transcription":
        # Handle transcription webhook with full conversation data
        handle_transcription_webhook(request_body["data"])
    elif request_body["type"] == "post_call_audio":
        # Handle audio webhook with minimal data
        handle_audio_webhook(request_body["data"])
    elif request_body["type"] == "call_initiation_failure":
        # Handle call initiation failure webhook
        handle_call_initiation_failure_webhook(request_body["data"])

    return web.json_response({"status": "ok"})

def handle_audio_webhook(data):
    # Decode base64 audio data
    audio_bytes = base64.b64decode(data["full_audio"])

    # Save or process the audio file
    conversation_id = data["conversation_id"]
    with open(f"conversation_{conversation_id}.mp3", "wb") as f:
        f.write(audio_bytes)

def handle_call_initiation_failure_webhook(data):
    # Handle call initiation failure events
    agent_id = data["agent_id"]
    conversation_id = data["conversation_id"]
    failure_reason = data.get("failure_reason")
    metadata = data.get("metadata", {})

    # Log the failure for monitoring
    print(f"Call failed for agent {agent_id}, conversation {conversation_id}")
    print(f"Failure reason: {failure_reason}")

    # Access provider-specific metadata
    provider_type = metadata.get("type")
    body = metadata.get("body", {})
    if provider_type == "sip":
        print(f"SIP status code: {body.get('sip_status_code')}")
        print(f"Error reason: {body.get('error_reason')}")
    elif provider_type == "twilio":
        print(f"Twilio CallSid: {body.get('CallSid')}")
        print(f"Call status: {body.get('CallStatus')}")

    # Update your system with the failure information
    # e.g., mark lead as "call_failed" in CRM

```

```javascript
import fs from "fs";

app.post("/webhook/elevenlabs", (req, res) => {
  let body = "";

  // Handle chunked/streaming requests
  req.on("data", (chunk) => {
    body += chunk;
  });

  req.on("end", () => {
    try {
      const requestBody = JSON.parse(body);

      // Process different webhook types
      if (requestBody.type === "post_call_transcription") {
        // Handle transcription webhook with full conversation data
        handleTranscriptionWebhook(requestBody.data);
      } else if (requestBody.type === "post_call_audio") {
        // Handle audio webhook with minimal data
        handleAudioWebhook(requestBody.data);
      } else if (requestBody.type === "call_initiation_failure") {
        // Handle call initiation failure webhook
        handleCallFailureWebhook(requestBody.data);
      }

      res.status(200).json({ status: "ok" });
    } catch (error) {
      console.error("Error processing webhook:", error);
      res.status(400).json({ error: "Invalid JSON" });
    }
  });
});

function handleAudioWebhook(data) {
  // Decode base64 audio data
  const audioBytes = Buffer.from(data.full_audio, "base64");

  // Save or process the audio file
  const conversationId = data.conversation_id;
  fs.writeFileSync(`conversation_${conversationId}.mp3`, audioBytes);
}

function handleCallFailureWebhook(data) {
  // Handle call initiation failure events
  const { agent_id, conversation_id, failure_reason, metadata } = data;

  // Log the failure for monitoring
  console.log(`Call failed for agent ${agent_id}, conversation ${conversation_id}`);
  console.log(`Failure reason: ${failure_reason}`);

  // Access provider-specific metadata
  const body = metadata.body || {};
  if (metadata?.type === "sip") {
    console.log(`SIP status code: ${body.sip_status_code}`);
    console.log(`Error reason: ${body.error_reason}`);
  } else if (metadata?.type === "twilio") {
    console.log(`Twilio CallSid: ${body.CallSid}`);
    console.log(`Call status: ${body.CallStatus}`);
  }

  // Update your system with the failure information
  // e.g., mark lead as "call_failed" in CRM
}
```

> **Note**
>
> オーディオWebhookは大きなファイルになる場合があるため、Webhookエンドポイントがストリーミングリクエストを処理でき、
> 十分なメモリ／ストレージ容量を備えていることを確認してください。オーディオはMP3形式で配信されます。

## ユースケース

### 通話後の自動フォローアップ

通話後Webhookを使用すると、通話終了直後にトリガーされる自動ワークフローを構築できます。実用的な活用例をいくつか紹介します：

#### CRMインテグレーション

通話が完了するとすぐに、会話データで顧客関係管理システムを更新します：

```javascript
// Example webhook handler
app.post("/webhook/elevenlabs", async (req, res) => {
  // HMAC validation code

  const { data } = req.body;

  // Extract key information
  const userId = data.metadata.user_id;
  const transcriptSummary = data.analysis.transcript_summary;
  const callSuccessful = data.analysis.call_successful;

  // Update CRM record
  await updateCustomerRecord(userId, {
    lastInteraction: new Date(),
    conversationSummary: transcriptSummary,
    callOutcome: callSuccessful,
    fullTranscript: data.transcript,
  });

  res.status(200).send("Webhook received");
});
```

### ステートフルな会話

状態を保存・取得することで、複数回のやり取りにわたって会話コンテキストを維持します：

1. 通話開始時に、ユーザーIDを動的変数として渡します。
2. 通話終了時に、dynamic_variablesから抽出したユーザーIDに基づいて会話データをデータベースに保存するよう、Webhookエンドポイントを設定します。
3. ユーザーが再度通話した際に、このコンテキストを取得し、\{\{previous_topics}}動的変数として新しい会話に渡すことができます。
4. これにより、エージェントが以前のやり取りを「記憶」しているようなシームレスな体験を実現できます。

```javascript
// Store conversation state when call ends
app.post("/webhook/elevenlabs", async (req, res) => {
  // HMAC validation code

  const { data } = req.body;
  const userId = data.metadata.user_id;

  // Store conversation state
  await db.userStates.upsert({
    userId,
    lastConversationId: data.conversation_id,
    lastInteractionTimestamp: data.metadata.start_time_unix_secs,
    conversationHistory: data.transcript,
    previousTopics: extractTopics(data.analysis.transcript_summary),
  });

  res.status(200).send("Webhook received");
});

// When initiating a new call, retrieve and use the state
async function initiateCall(userId) {
  // Get user's conversation state
  const userState = await db.userStates.findOne({ userId });

  // Start new conversation with context from previous calls
  return await elevenlabs.startConversation({
    agent_id: "xyz",
    conversation_id: generateNewId(),
    dynamic_variables: {
      user_name: userState.name,
      previous_conversation_id: userState.lastConversationId,
      previous_topics: userState.previousTopics.join(", "),
    },
  });
}
```