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

Webhooks

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:

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"
}
CampoTipoDescripción
call_idintegerSe mantiene estable en todos los eventos de esta llamada
directionstringinbound, outbound, web, test. Las cargas útiles históricas pueden contener los valores heredados mic o widget
from_number, to_numberstringE.164. from_number es literalmente "web" para las llamadas web y las llamadas de prueba
origin_domainstringSolo web/prueba: el origen de la página que alojó el widget (vacío para sesiones de micrófono)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullSe deriva de inicio/fin
statusstringcompleted o failed
end_reasonstringConsulta la tabla a continuación
product, voicestringConfiguración del agente en vigor al momento 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
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 siguen informando aquí sus minutos medidos reales, pero el cargo se limita a un minuto según 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

Motivos de finalización

ValorSignificado
user_hangupLa otra parte colgó primero
ai_hangupLa IA finalizó la llamada deliberadamente
ai_transferLa IA transfirió la llamada; se establece transfer_number
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
supersededLa sesión fue reemplazada por una más reciente
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 (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"] }
    }
  }
]
CampoTipoDescripción
rolecadenauser, model, tool o system
content_typecadenatext/plain para voz; application/json para llamadas a herramientas, resultados de herramientas y eventos del sistema
contentcadena | objetoTexto 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_msenteroDesplazamientos desde el inicio de la llamada, en ms. Se incluyen cuando se conoce la duración del audio
ttfa_msenteroTiempo hasta el primer audio de un turno model, cuando se mide
audio_url, audio_urlscadena / matrizURL 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 en transcripts (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") 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 de endpoints las asigna a web.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

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 los registros de tus clientes.

Analítica

Transmite las transcripciones a una plataforma para modelado de temas, extracción de señales de CSAT o monitoreo de la tasa de transferencias.

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


Relacionado