---
title: "telephony.complete / web.complete"
description: "Webhook no bloqueante enviado cuando finaliza una llamada, con transcripción, URL de grabación y métricas."
---

Un evento de finalización se activa después de que termina cada llamada: telefonía entrante,
telefonía saliente, llamada web o llamada de prueba (sesión de micrófono del builder). Es
**sin bloqueo**: responde con cualquier código 2xx.

El evento se entrega por ambas rutas:

* Los **[endpoints de webhook](/es/webhooks/endpoints)** reciben
  `telephony.complete` (llamadas telefónicas) o `web.complete` (llamadas web y
  llamadas de prueba con micrófono del builder) con la carga útil estable documentada a continuación,
  un `event_id` por entrega, un tiempo de espera de 30 s y
  [reintentos durante un máximo de 24 h](/es/webhooks/overview).
* El **[webhook heredado de URL única](/api-reference/organizations#legacy-single-url-webhook)**
  recibe un intento síncrono (tiempo de espera de 10 s, sin reintentos) con una
  carga útil ligeramente diferente; consulta
  [Diferencias de la carga útil heredada](#legacy-payload-differences).

## Carga útil de la solicitud (entregas de endpoint)

```json
{
  "data": {
    "billable_minutes": 1.25,
    "billing_total_cents": 8,
    "call_id": 987654321,
    "direction": "inbound",
    "duration_seconds": 54,
    "end_reason": "user_hangup",
    "end_time": "2026-04-20T18:25:04.822Z",
    "extracted_data": {
      "status": "completed",
      "fields": {
        "customer_name": "Alex Morgan",
        "appointment_date": "2026-04-23"
      },
      "evidence": {
        "customer_name": {
          "quote": "My name is Alex Morgan",
          "speaker_role": "caller",
          "turn_index": 4
        },
        "appointment_date": {
          "quote": "April 23 works for me",
          "speaker_role": "caller",
          "turn_index": 7
        }
      },
      "verification": "verified",
      "field_reasons": {},
      "schema_version": "92850758e231a3c95a..."
    },
    "from_number": "+14155550199",
    "product": "spark",
    "recording_stopped_at": null,
    "recording_stopped_reason": null,
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
    "to_number": "+15551234567",
    "transcripts": [ /* see Transcript format */ ],
    "transfer_number": null,
    "unresolved_variables": ["campaign_owner"],
    "variables": {"campaign_name": "Spring renewals"},
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `call_id` | integer | Se mantiene estable en todos los eventos de esta llamada |
| `agent_id` | integer \| null | Agente que atendió la llamada, cuando se asignó uno |
| `agent_name` | string \| null | Agente que atendió la llamada, cuando se asignó uno |
| `direction` | string | `inbound`, `outbound`, `web`, `test`. Las cargas útiles históricas pueden contener valores heredados `mic` o `widget` |
| `from_number`, `to_number` | string | E.164. `from_number` es el valor literal `"web"` para llamadas web y llamadas de prueba |
| `origin_domain` | string | **Solo web/prueba** — el origen de la página que alojaba el widget (vacío para sesiones de micrófono) |
| `start_time`, `end_time` | timestamp | ISO 8601 UTC |
| `duration_seconds` | integer \| null | Derivado del inicio/fin |
| `status` | string | `completed` o `failed` |
| `end_reason` | string | Consulta la tabla a continuación |
| `product`, `voice` | string | Configuración del agente vigente en el momento de la llamada |
| `variables` | object | Variables de entrada capturadas al inicio de la llamada |
| `unresolved_variables` | array | Nombres de variables a los que hace referencia la configuración de la llamada, pero que no se proporcionaron al inicio de la llamada |
| `transfer_number` | string \| null | Se establece cuando se transfirió la llamada |
| `recording_url` | string \| null | URL firmada con vencimiento; descárgala de inmediato. `null` cuando no hay ningún artefacto de grabación disponible |
| `recording_stopped_at` | timestamp \| null | Momento en que se detuvo la grabación antes de que terminara la llamada; nada posterior a este momento está en la grabación. `null` cuando la grabación continuó hasta el final de la llamada o la llamada no se grabó |
| `recording_stopped_reason` | string \| null | Motivo por el que la grabación se detuvo antes de tiempo. `warm_transfer`: se detiene justo antes de que una [transferencia asistida](/es/guides/call-transfers) llame al destino y permanece desactivada incluso si la transferencia falla y quien llama regresa al agente. Trata otros valores como informativos; podrían agregarse más. `null` cuando `recording_stopped_at` es `null` |
| `billable_minutes` | number | Minutos facturados, redondeados al cuarto de minuto más cercano (incrementos de 15 segundos, mínimo 0.25). Las llamadas que van directamente al buzón de voz aún informan aquí sus minutos reales medidos, pero el cargo tiene un límite de un minuto a la tarifa del plan. |
| `billing_total_cents` | integer | Centavos de USD |
| `transcripts` | array | Entradas de transcripción por turno; puede estar vacío cuando no hay una transcripción disponible |
| `summary` | string \| null | Resumen posterior a la llamada en lenguaje sencillo (de dos a cuatro oraciones de prosa, con un máximo de 2,000 caracteres, escrito en el idioma de la llamada: el idioma principal del agente, salvo que la llamada lo haya reemplazado) cuando el agente tiene activada la opción **Generar un resumen de llamada** en **Datos para recopilar** en el creador de agentes (`call_summary_enabled` en el [Agente](/api-reference/agents)) y la generación fue exitosa; `null` en caso contrario, incluidas las llamadas demasiado cortas para resumir, las llamadas sin transcripción y los agentes con la grabación desactivada. El resumen se genera en el mismo paso posterior a la llamada que la extracción, por lo que la configuración **Esperar los datos extraídos antes de enviar el webhook de finalización de llamada** (`extraction_blocks_completion`) determina si este evento lo espera: en un modo de espera, el resumen está listo antes de que se envíe este evento (el resumen tiene un máximo de 30 segundos dentro del presupuesto de 75 segundos del paso); cuando el evento no espera (`never`, o una transferencia bajo `except_transfers`), este campo es `null` y el resumen **no** se reenvía en `call.data_extracted`, así que léelo más tarde desde `summary` en el [objeto Call](/api-reference/calls#call-object). También está disponible en la página de llamadas del dashboard |
| `extracted_data` | object \| null | Resultado de extracción estructurada con `status`, `fields`, `evidence`, `verification`, `field_reasons` y `schema_version`. Cada campo distinto de nulo incluye la cita exacta verificada estructuralmente (como máximo 1,000 caracteres), además de su rol de hablante y su índice de turno; las citas más largas devueltas por el modelo se rechazan en lugar de truncarse. La evidencia es `null` cuando el campo es `null`. `verified` significa que cada candidato recibió exactamente un veredicto independiente válido. `unavailable` también incluye resultados del verificador malformados o parciales; los veredictos parciales válidos se siguen aplicando, mientras que los candidatos sin un veredicto válido se establecen en nulo. `status` es `completed`, `failed`, `exhausted`, `skipped` o `skipped_recording_disabled`; `null` cuando el agente no tenía campos de extracción |
| `steps_trace` | array | Solo en llamadas cuyo agente tiene pasos o derivaciones. El orden en que la llamada pasó por ellos: cada entrada tiene `t_ms` (milisegundos desde que comenzó la llamada), `agent` (la clave del agente, `a` más su ID), `step` (el ID del paso, o `null` en una derivación a un agente sin pasos) y `via` (`start`, `model`, `condition`, `on_enter` o `handoff`). Solo contiene ID y horas, por lo que se conserva incluso cuando la grabación está desactivada |
| `collected_values` | object | Solo en llamadas cuyo agente tiene pasos o derivaciones. Los valores que el agente recopiló en sus pasos, indexados por nombre de campo de recopilación, además de `<tool>_result` para el resultado de `on_enter_tool` de cada paso. Se omite cuando el contenido de la llamada no se conserva (grabación desactivada) |

### Motivos de finalización

| Valor | Significado |
|-------|---------|
| `user_hangup` | La parte remota colgó primero |
| `ai_hangup` | La IA finalizó la llamada deliberadamente |
| `ai_transfer` | La IA transfirió la llamada; `transfer_number` está configurado |
| `ai_warm_transfer` | La IA completó una transferencia asistida |
| `voicemail_hangup` | Se detectó el buzón de voz y la llamada finalizó según tu `voicemail_action` |
| `max_duration` | La llamada alcanzó el límite máximo de duración: el del agente, el de la propia llamada o el límite de la plataforma de 6 horas. Una llamada que la IA o quien llama finaliza después de la advertencia de límite de tiempo conserva `ai_hangup` o `user_hangup` |
| `superseded` | La sesión fue reemplazada por una más reciente |
| `inbound_config_webhook_failed` | Llamada entrante a un número en modo webhook: el [webhook de configuración](/es/webhooks/call-incoming) no devolvió una configuración utilizable, por lo que la llamada finalizó antes de que un agente respondiera. `status` es `failed` y la llamada no se factura |
| `unknown` | No se pudo determinar el motivo de finalización |

## Formato de transcripción

Cada entrada en `transcripts` corresponde a un turno conversacional. Los roles son
`user` (habla de quien llama), `model` (habla del agente **y** llamadas a herramientas),
`tool` (resultados de herramientas) y `system` (eventos de llamada, como cambios
de idioma).

```json
[
  {
    "role": "user",
    "content_type": "text/plain",
    "content": "Hi, I'm calling about my appointment.",
    "start_ms": 1200,
    "end_ms":   4100,
    "audio_url": "https://storage.example.com/…"
  },
  {
    "role": "model",
    "content_type": "text/plain",
    "content": "Sure, what date works best?",
    "start_ms": 4200,
    "end_ms":   6100
  },
  {
    "role": "model",
    "content_type": "application/json",
    "content": {
      "tool_call": "search_appointments",
      "arguments": { "date": "2026-04-21" }
    }
  },
  {
    "role": "tool",
    "content_type": "application/json",
    "content": {
      "tool_name": "search_appointments",
      "response": { "available_slots": ["9:00 AM", "2:00 PM"] }
    }
  }
]
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `role` | cadena | `user`, `model`, `tool` o `system` |
| `content_type` | cadena | `text/plain` para habla; `application/json` para llamadas a herramientas, resultados de herramientas y eventos del sistema |
| `content` | cadena \| objeto | Texto de habla o el objeto estructurado que se muestra arriba. Llamadas a herramientas: `{"tool_call": name, "arguments": {…}}`. Resultados de herramientas: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | entero | Desplazamientos desde el inicio de la llamada, en ms. Se incluyen cuando se conoce la temporización del audio |
| `ttfa_ms` | entero | Tiempo hasta el primer audio para un turno de `model`, cuando se mide |
| `audio_url`, `audio_urls` | cadena / arreglo | URL firmadas con vencimiento para el audio del turno, cuando se graba por turno |

Para obtener el historial de turnos completamente estructurado (con marcadores de
interrupción, prompts de confirmación y posiciones sin procesar), usa
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

## Diferencias de la carga útil heredada

El sobre de webhook heredado de URL única es
`{"type": "telephony.complete" | "web.complete", "data": {…}}` sin
**`event_id`**, y sus `data` difieren de la carga útil del endpoint:

La carga útil de finalización heredada también incluye `agent_id` y `agent_name`.

* El arreglo de turnos está en **`history`**, no en `transcripts` (con el mismo
  esquema de turnos que se muestra arriba).
* El conjunto de campos es el informe sin procesar al final de la llamada y puede incluir
  campos internos adicionales además de los de la tabla anterior; trata los campos desconocidos
  como informativos.
* Las llamadas web (`direction: "web"`) **omiten** `from_number` / `to_number`
  y agregan `origin_domain`.
* Las llamadas de prueba de micrófono del Builder se informan como `telephony.complete` en la ruta
  heredada (el sistema del endpoint las asigna a `web.complete`).
* **Coordinación de transferencia:** cuando una llamada termina en una transferencia, el
  webhook heredado se llama de forma síncrona y puede responder
  `{"transfer_ready": false}` para indicar que el destino de la transferencia no está
  listo. Cualquier otra respuesta (o la ausencia de un webhook heredado) permite que la transferencia
  continúe. Las entregas del endpoint nunca se consultan para esto.

---

## Ejemplo de controlador

<CodeGroup>
```python Python (FastAPI)
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)

    event = json.loads(body)
    if event["type"] in ("telephony.complete", "web.complete"):
        data = event["data"]
        # Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
        turns = data.get("transcripts") or data.get("history") or []
        await persist_call_record(
            call_id=data["call_id"],
            turns=turns,
            recording_url=data.get("recording_url"),
        )
        if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
            await notify_team(data.get("transfer_number"), data["call_id"])
    return {"ok": True}
```

```javascript Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

function verify(body, signature) {
  const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
  return signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    if (["telephony.complete", "web.complete"].includes(event.type)) {
      const data = event.data;
      // Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
      const turns = data.transcripts ?? data.history ?? [];
      await persistCallRecord({ ...data, turns });
      if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
        await notifyTeam(data.transfer_number, data.call_id);
      }
    }
    res.json({ ok: true });
  },
);
```
</CodeGroup>

---

## Casos de uso comunes

<CardGroup cols={2}>
  <Card title="Integración con CRM" icon="database">
    Guarda la transcripción y la URL de grabación de cada llamada junto con tus
    registros de clientes.
  </Card>
  <Card title="Analítica" icon="chart-line">
    Transmite transcripciones a una plataforma para modelado de temas, extracción
    de señales de CSAT o monitoreo de tasas de transferencia.
  </Card>
  <Card title="Revisión de calidad" icon="clipboard-check">
    Abre llamadas en una herramienta de control de calidad para revisión humana o ejecútalas con tu
    propio modelo de evaluación.
  </Card>
  <Card title="Notificaciones" icon="bell">
    Notifica a un integrante humano del equipo ante una transferencia o un error.
  </Card>
</CardGroup>

---

## Relacionado

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/es/webhooks/call-incoming">
    La contraparte bloqueante que se ejecuta al inicio de la llamada.
  </Card>
  <Card title="Catálogo de eventos" icon="list" href="/es/webhooks/events">
    Otros tipos de eventos a los que puedes suscribirte.
  </Card>
  <Card title="API de historial de llamadas" icon="phone" href="/api-reference/calls">
    Los mismos datos accesibles mediante REST para completar datos históricos o reproducirlos.
  </Card>
</CardGroup>
