telephony.complete / web.complete
Webhook sin bloqueo que se entrega cuando finaliza una llamada, con transcripción, URL de la 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 no bloqueante: 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 hasta 24 h. - El webhook heredado de URL única recibe un intento sincrónico (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 endpoints)
{
"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",
"from_number": "+14155550199",
"product": "spark",
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"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 |
direction | string | inbound, outbound, web, test. Las cargas útiles históricas pueden contener los valores heredados mic o widget |
from_number, to_number | string | E.164. from_number es literalmente "web" para las llamadas web y las llamadas de prueba |
origin_domain | string | Solo web/prueba: el origen de la página que alojó el widget (vacío para sesiones de micrófono) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Se deriva de inicio/fin |
status | string | completed o failed |
end_reason | string | Consulta la tabla a continuación |
product, voice | string | Configuración del agente en vigor al momento 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 |
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 siguen informando aquí sus minutos medidos reales, pero el cargo se limita a un minuto según 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 |
Motivos de finalización
| Valor | Significado |
|---|---|
user_hangup | La otra parte colgó primero |
ai_hangup | La IA finalizó la llamada deliberadamente |
ai_transfer | La IA transfirió la llamada; se establece transfer_number |
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 |
superseded | La sesión fue reemplazada por una más reciente |
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 (voz de quien llama), model (voz 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 voz; application/json para llamadas a herramientas, resultados de herramientas y eventos del sistema |
content | cadena | objeto | Texto de voz 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 duración del audio |
ttfa_ms | entero | Tiempo hasta el primer audio de un turno model, cuando se mide |
audio_url, audio_urls | cadena / matriz | URL firmadas temporales para el audio del turno, cuando se graba por turno |
Para 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 del 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 matriz 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 de fin de llamada y puede incluir campos internos adicionales aparte 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 de endpoints las asigna aweb.complete). - Coordinación de transferencias: cuando una llamada termina en una transferencia,
el webhook heredado se llama de forma sincrónica 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 los registros de tus clientes.
Transmite las transcripciones a una plataforma para modelado de temas, extracción de señales de CSAT o monitoreo de la tasa de transferencias.
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 fallo.