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

# Webhooks post-llamada

## Resumen

Los [webhooks](/docs/es/eleven-api/resources/webhooks) post-llamada te permiten recibir información detallada sobre una llamada después de que se complete el análisis. Cuando están activados, ElevenLabs enviará una solicitud POST a la ruta que especifiques con datos completos de la llamada.

ElevenLabs admite tres tipos de webhooks post-llamada:

* **Webhooks de transcripción** (`post_call_transcription`): contienen datos completos de la conversación, incluidas las transcripciones, los resultados del análisis y los metadatos
* **Webhooks de audio** (`post_call_audio`): contienen datos mínimos con el audio de toda la conversación codificado en base64
* **Webhooks de fallo al iniciar llamadas** (`call_initiation_failure`): contienen información sobre intentos fallidos de iniciar llamadas, incluidos los motivos del fallo y los metadatos

## Activar webhooks post-llamada

Los webhooks post-llamada se pueden activar para todos los agentes de tu espacio de trabajo desde la [página de configuración](https://elevenlabs.io/app/agents/settings) de ElevenAgents.

![Configuración del webhook post-llamada](/docs/_fern-img/eb5d768612d6461a21bc3127611f60724be3e1a55005af43faf48f2d7bf23807.webp)

> **Warning**
>
> Los webhooks post-llamada deben devolver un código de estado 200 para considerarse correctos. Los webhooks que
> fallan repetidamente se desactivan automáticamente si hay 10 o más fallos consecutivos y la última
> entrega correcta fue hace más de 7 días o nunca se han entregado correctamente.

> **Note**
>
> Los webhooks post-llamada pueden reintentarse automáticamente si fallan. Consulta los [reintentos de webhooks](/docs/es/eleven-api/resources/webhooks#retries).

### Autenticación

Es importante que el listener valide todos los webhooks entrantes. Actualmente, los webhooks admiten autenticación mediante firmas HMAC. Configura la autenticación HMAC de esta forma:

* Almacena de forma segura el secreto compartido generado al crear el webhook
* Verifica la cabecera ElevenLabs-Signature en tu ruta de API mediante el SDK

El SDK de JavaScript incluye `constructEvent`; el SDK de Python incluye `construct_event` con **`rawBody`**, **`sig_header`** y **`secret`** (en Python no se llaman `payload` / `signature`). Ambos verifican la firma, validan la marca de tiempo y analizan la carga útil JSON.

#### Python

Ejemplo de controlador de webhook con FastAPI:

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

Ejemplo de controlador de webhook con Express:

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

Ejemplo de controlador de webhook con una ruta de API de Next.js:

**`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 });
}
```

### Lista de permitidos de IP

Para mayor seguridad, puedes añadir las IP de salida estáticas de ElevenLabs a tu lista de permitidos. Consulta [Lista de permitidos de IP](/docs/es/eleven-api/resources/ip-allowlisting) para ver la lista completa de direcciones IP.

> **Tip**
>
> Usar una lista de permitidos de IP junto con la validación de firmas HMAC proporciona varias capas de
> seguridad.

## Estructura de respuesta del webhook

ElevenLabs envía tres tipos distintos de webhooks posteriores a la llamada, cada uno con estructuras de datos diferentes:

### Webhooks de transcripción (`post_call_transcription`)

Contiene datos completos de la conversación, incluidas las transcripciones completas, los resultados del análisis y los metadatos.

#### Campos de nivel superior

| Campo             | Tipo   | Descripción                                                                 |
| ----------------- | ------ | --------------------------------------------------------------------------- |
| `type`            | string | Tipo de evento (siempre `post_call_transcription`)                          |
| `data`            | object | Datos de la conversación con la estructura `ConversationHistoryCommonModel` |
| `event_timestamp` | number | Cuándo ocurrió este evento en hora Unix UTC                                 |

#### Estructura del objeto de datos

El objeto `data` contiene:

| Campo                                 | Tipo    | Descripción                                                            |
| ------------------------------------- | ------- | ---------------------------------------------------------------------- |
| `agent_id`                            | string  | El ID del agente que atendió la llamada                                |
| `agent_name`                          | string  | El nombre del agente en el momento de la conversación                  |
| `conversation_id`                     | string  | Identificador único de la conversación                                 |
| `status`                              | string  | Estado de la conversación (p. ej., "done")                             |
| `user_id`                             | string  | Identificador de usuario, si está disponible                           |
| `branch_id`                           | string  | La rama del agente utilizada para la conversación, si corresponde      |
| `version_id`                          | string  | El ID de la versión del agente (instantánea) activa durante la llamada |
| `environment`                         | string  | El entorno utilizado para resolver las variables de entorno            |
| `transcript`                          | array   | Transcripción completa de la conversación con los turnos               |
| `metadata`                            | object  | Horarios, costes y detalles telefónicos de la llamada                  |
| `analysis`                            | object  | Resultados de la evaluación y resumen de la conversación               |
| `conversation_initiation_client_data` | object  | Anulaciones de configuración y variables dinámicas                     |
| `has_audio`                           | boolean | Si hay audio disponible para la conversación                           |
| `has_user_audio`                      | boolean | Si hay audio del usuario disponible para la conversación               |
| `has_response_audio`                  | boolean | Si hay audio de respuesta del agente disponible para la conversación   |

### Webhooks de audio (`post_call_audio`)

Contiene datos mínimos junto con el audio completo de la conversación en formato MP3 codificado en base64.

#### Campos de nivel superior

| Campo             | Tipo   | Descripción                                 |
| ----------------- | ------ | ------------------------------------------- |
| `type`            | string | Tipo de evento (siempre `post_call_audio`)  |
| `data`            | object | Datos mínimos de audio                      |
| `event_timestamp` | number | Cuándo ocurrió este evento en hora Unix UTC |

#### Estructura del objeto de datos

El objeto `data` contiene únicamente:

| Campo             | Tipo   | Descripción                                                                         |
| ----------------- | ------ | ----------------------------------------------------------------------------------- |
| `agent_id`        | string | El ID del agente que atendió la llamada                                             |
| `conversation_id` | string | Identificador único de la conversación                                              |
| `full_audio`      | string | Cadena codificada en base64 con el audio completo de la conversación en formato MP3 |

> **Warning**
>
> Los webhooks de audio contienen únicamente los tres campos indicados arriba. NO incluyen datos de
> transcripción, metadatos, resultados de análisis ni ningún otro detalle de la conversación.

### Webhooks de error de inicio de llamada (`call_initiation_failure`)

Contiene información sobre los intentos de iniciar llamadas telefónicas, incluidos los motivos del error y los metadatos del proveedor de telefonía.

> **Note**
>
> Los eventos de webhook de error de inicio de llamada se envían cuando una llamada no se puede iniciar por
> errores de conexión, porque el usuario rechaza la llamada o porque no la contesta. Si una llamada va al buzón de voz o la
> responde un servicio automatizado, no se envía ningún webhook de error de inicio de llamada, ya que la llamada se
> inició correctamente.

#### Campos de nivel superior

| Campo             | Tipo   | Descripción                                        |
| ----------------- | ------ | -------------------------------------------------- |
| `type`            | string | Tipo de evento (siempre `call_initiation_failure`) |
| `data`            | object | Datos del error de inicio de llamada               |
| `event_timestamp` | number | Cuándo ocurrió este evento en hora Unix UTC        |

#### Estructura del objeto de datos

El objeto `data` contiene:

| Campo             | Tipo   | Descripción                                                     |
| ----------------- | ------ | --------------------------------------------------------------- |
| `agent_id`        | string | El ID del agente asignado para atender la llamada               |
| `conversation_id` | string | Identificador único de la conversación                          |
| `failure_reason`  | string | El motivo del error ("busy", "no-answer", "unknown")            |
| `metadata`        | object | Datos adicionales proporcionados por el proveedor de telefonía. |

#### Estructura del objeto de metadatos

La estructura del objeto `metadata` varía según si la llamada saliente se realizó mediante Twilio o mediante trunking SIP. El objeto incluye un campo `type` que distingue entre ambos y un campo `body` con detalles específicos del proveedor.

**Metadatos de SIP** (`type: "sip"`):

| Campo  | Tipo   | Obligatorio | Descripción                                        |
| ------ | ------ | ----------- | -------------------------------------------------- |
| `type` | string | Sí          | Tipo de proveedor (siempre `sip`)                  |
| `body` | object | Sí          | Información del error de llamada específica de SIP |

El objeto `body` de los metadatos de SIP contiene:

| Campo             | Tipo   | Obligatorio | Descripción                                                                                                |
| ----------------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `from_number`     | number | Sí          | El número de teléfono de la parte que inició la llamada.                                                   |
| `to_number`       | number | Sí          | El número de teléfono de la parte llamada.                                                                 |
| `sip_status_code` | number | Sí          | Código de estado de respuesta SIP (p. ej., 486 si está ocupado)                                            |
| `error_reason`    | string | Sí          | Descripción del error legible para humanos                                                                 |
| `call_sid`        | string | Sí          | Identificador de la sesión de llamada SIP                                                                  |
| `twirp_code`      | string | No          | [Código de error de Twirp](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes), si corresponde |
| `sip_status`      | string | No          | Texto de estado SIP correspondiente al código de estado                                                    |

**Metadatos de Twilio** (`type: "twilio"`):

| Campo  | Tipo   | Obligatorio | Descripción                                                                                                                                           |
| ------ | ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Sí          | Tipo de proveedor (siempre `twilio`)                                                                                                                  |
| `body` | object | Sí          | Cuerpo de StatusCallback de Twilio con detalles de la llamada, documentado [aquí](https://www.twilio.com/docs/voice/api/call-resource#statuscallback) |

## Ejemplos de cargas útiles de webhooks

### Ejemplo de webhook de transcripción

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

### Ejemplo de webhook de audio

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

### Ejemplos de webhook de error de inicio de llamada

#### Ejemplo de metadatos de 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"
      }
    }
  }
}
```

#### Ejemplo de metadatos de 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"
      }
    }
  }
}
```

## Entrega de webhooks de audio

Los webhooks de audio se entregan por separado de los webhooks de transcripción y contienen únicamente los campos esenciales para identificar la conversación, junto con los datos de audio codificados en base64.

> **Note**
>
> Los webhooks de audio se pueden activar o desactivar con el interruptor «Enviar datos de audio» de la configuración
> del webhook. Esta opción se puede configurar tanto a nivel de espacio de trabajo (en la configuración de ElevenAgents)
> como a nivel de agente (en las anulaciones de webhook de cada agente).

### Entrega en streaming

Los webhooks de audio se entregan como solicitudes HTTP en streaming con la cabecera `transfer-encoding: chunked` para gestionar archivos de audio grandes de forma eficiente. Cada solicitud agota el tiempo de espera tras 5 minutos.

### Reintentos

Cuando los reintentos están activados en el webhook, las entregas de audio fallidas se vuelven a intentar según el mismo calendario que los webhooks de transcripción. Un reintento reenvía la carga útil completa de audio, así que elimina duplicados por `conversation_id`. Consulta [reintentos de webhooks](/docs/es/eleven-api/resources/webhooks#retries) para ver el calendario, los errores que se pueden reintentar y los límites de tamaño de audio.

### Procesamiento de webhooks de audio

Dado que los webhooks de audio se entregan mediante codificación de transferencia por bloques, tendrás que gestionar correctamente los datos en streaming:

```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**
>
> Los webhooks de audio pueden ser archivos grandes, así que asegúrate de que tu ruta de API de webhook pueda gestionar solicitudes en streaming
> y tenga suficiente capacidad de memoria y almacenamiento. El audio se entrega en formato MP3.

## Casos de uso

### Seguimientos de llamadas automatizados

Los webhooks posteriores a la llamada te permiten crear workflows automatizados que se activan inmediatamente después de que termine una llamada. Estas son algunas aplicaciones prácticas:

#### Integración con CRM

Actualiza tu sistema de gestión de relaciones con clientes con los datos de la conversación en cuanto finalice una llamada:

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

### Conversaciones con estado

Mantén el contexto de la conversación en varias interacciones almacenando y recuperando el estado:

1. Cuando empiece una llamada, transmite tu ID de usuario como variable dinámica.
2. Cuando termine una llamada, configura tu ruta de API de webhook para almacenar los datos de la conversación en tu base de datos según el ID de usuario extraído de `dynamic_variables`.
3. Cuando el usuario vuelva a llamar, podrás recuperar este contexto y transmitirlo a la nueva conversación en una variable dinámica \{\{previous_topics}}.
4. Esto crea una experiencia fluida en la que el agente «recuerda» interacciones anteriores.

```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(", "),
    },
  });
}
```