ThunderPhone 2.0 ya está disponible.Empieza por tu cuenta desde 2¢/min.Lee el anuncio

Webhooks

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:

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"
}
CampoTipoDescripción
call_idintegerSe mantiene estable en todos los eventos de esta llamada
agent_idinteger | nullAgente que atendió la llamada, cuando se asignó uno
agent_namestring | nullAgente que atendió la llamada, cuando se asignó uno
directionstringinbound, outbound, web, test. Las cargas útiles históricas pueden contener valores heredados mic o widget
from_number, to_numberstringE.164. from_number es el valor literal "web" para llamadas web y llamadas de prueba
origin_domainstringSolo web/prueba — el origen de la página que alojaba el widget (vacío para sesiones de micrófono)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullDerivado del inicio/fin
statusstringcompleted o failed
end_reasonstringConsulta la tabla a continuación
product, voicestringConfiguración del agente vigente en el momento de la llamada
variablesobjectVariables de entrada capturadas al inicio de la llamada
unresolved_variablesarrayNombres de variables a los que hace referencia la configuración de la llamada, pero que no se proporcionaron al inicio de la llamada
transfer_numberstring | nullSe establece cuando se transfirió la llamada
recording_urlstring | nullURL firmada con vencimiento; descárgala de inmediato. null cuando no hay ningún artefacto de grabación disponible
recording_stopped_attimestamp | nullMomento 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_reasonstring | nullMotivo 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_minutesnumberMinutos 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_centsintegerCentavos de USD
transcriptsarrayEntradas de transcripción por turno; puede estar vacío cuando no hay una transcripción disponible
summarystring | nullResumen 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_dataobject | nullResultado 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_tracearraySolo 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_valuesobjectSolo 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

ValorSignificado
user_hangupLa parte remota colgó primero
ai_hangupLa IA finalizó la llamada deliberadamente
ai_transferLa IA transfirió la llamada; transfer_number está configurado
ai_warm_transferLa IA completó una transferencia asistida
voicemail_hangupSe detectó el buzón de voz y la llamada finalizó según tu voicemail_action
max_durationLa 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
supersededLa sesión fue reemplazada por una más reciente
inbound_config_webhook_failedLlamada 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
unknownNo 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"] }
    }
  }
]
CampoTipoDescripción
rolecadenauser, model, tool o system
content_typecadenatext/plain para habla; application/json para llamadas a herramientas, resultados de herramientas y eventos del sistema
contentcadena | objetoTexto 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_msenteroDesplazamientos desde el inicio de la llamada, en ms. Se incluyen cuando se conoce la temporización del audio
ttfa_msenteroTiempo hasta el primer audio para un turno de model, cuando se mide
audio_url, audio_urlscadena / arregloURL 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 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

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

Casos de uso comunes

Integración con CRM

Guarda la transcripción y la URL de grabación de cada llamada junto con tus registros de clientes.

Analítica

Transmite transcripciones a una plataforma para modelado de temas, extracción de señales de CSAT o monitoreo de tasas de transferencia.

Revisión de calidad

Abre llamadas en una herramienta de control de calidad para revisión humana o ejecútalas con tu propio modelo de evaluación.

Notificaciones

Notifica a un integrante humano del equipo ante una transferencia o un error.


Relacionado