Open in
telephony.complete / web.complete
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 reciben
telephony.complete(llamadas telefónicas) oweb.complete(llamadas web y llamadas de prueba con micrófono del builder) con la carga útil estable documentada a continuación, unevent_idpor entrega, un tiempo de espera de 30 s y reintentos durante un máximo de 24 h. - El webhook heredado de URL única 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.
Carga útil de la solicitud (entregas de endpoint)
{
"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 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) 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. 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 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).
[
{
"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.
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 entranscripts(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") omitenfrom_number/to_numbery agreganorigin_domain. - Las llamadas de prueba de micrófono del Builder se informan como
telephony.completeen la ruta heredada (el sistema del endpoint las asigna aweb.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
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}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 });
},
);Casos de uso comunes
Guarda la transcripción y la URL de grabación de cada llamada junto con tus registros de clientes.
Transmite transcripciones a una plataforma para modelado de temas, extracción de señales de CSAT o monitoreo de tasas de transferencia.
Abre llamadas en una herramienta de control de calidad para revisión humana o ejecútalas con tu propio modelo de evaluación.
Notifica a un integrante humano del equipo ante una transferencia o un error.