ThunderPhone 2.0 já está no ar.Contrate online, a partir de 2¢/min.Leia o anúncio

Webhooks

telephony.complete / web.complete

Webhook não bloqueante entregue quando uma chamada termina, com transcrição, URL da gravação e métricas.

Um evento de conclusão é disparado após o término de cada chamada — telefonia de entrada, telefonia de saída, chamada web ou chamada de teste (sessão de microfone no builder). Ele é não bloqueante: responda com qualquer código 2xx.

O evento é entregue por ambos os caminhos:

Corpo da solicitação (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"
}
CampoTipoDescrição
call_idinteiroEstável em todos os eventos desta chamada
agent_idinteiro | nuloAgente que atendeu a chamada, quando um foi atribuído
agent_namestring | nuloAgente que atendeu a chamada, quando um foi atribuído
directionstringinbound, outbound, web, test. Cargas úteis históricas podem conter os valores legados mic ou widget
from_number, to_numberstringE.164. from_number é o literal "web" para chamadas pela web e chamadas de teste
origin_domainstringSomente web/teste — a origem da página que hospedou o widget (vazia para sessões de microfone)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteiro | nuloDerivado do início/fim
statusstringcompleted ou failed
end_reasonstringConsulte a tabela abaixo
product, voicestringConfiguração do agente em vigor no momento da chamada
variablesobjetoVariáveis de entrada registradas no início da chamada
unresolved_variablesarrayNomes de variáveis referenciados pela configuração da chamada, mas não fornecidos no início da chamada
transfer_numberstring | nuloDefinido quando a chamada foi transferida
recording_urlstring | nuloURL assinada com expiração; faça o download imediatamente. null quando não há artefato de gravação disponível
recording_stopped_attimestamp | nuloQuando a gravação foi interrompida antes do fim da chamada; nada após esse momento está na gravação. null quando a gravação continuou até o fim da chamada ou a chamada não foi gravada
recording_stopped_reasonstring | nuloMotivo pelo qual a gravação foi interrompida antecipadamente. warm_transfer: ela é interrompida pouco antes de uma transferência assistida ligar para o destino e permanece desativada mesmo se a transferência falhar e quem liga retornar ao agente. Considere outros valores apenas informativos; outros podem ser adicionados. null quando recording_stopped_at é null
billable_minutesnúmeroMinutos cobrados, arredondados para o quarto de minuto mais próximo (incrementos de 15 segundos, mínimo de 0.25). Chamadas que vão diretamente para a caixa postal ainda informam aqui seus minutos reais medidos, mas a cobrança é limitada a um minuto pela tarifa do plano.
billing_total_centsinteiroCentavos de USD
transcriptsarrayEntradas de transcrição por turno; pode estar vazio quando uma transcrição não estiver disponível
summarystring | nuloResumo pós-chamada em linguagem simples (de duas a quatro frases em prosa, com no máximo 2.000 caracteres, escrito no idioma da chamada: o idioma principal do agente, a menos que a chamada o tenha substituído) quando o agente tiver Gerar um resumo da chamada ativado em Dados a coletar no criador de agentes (call_summary_enabled no Agente) e a geração tiver sido bem-sucedida; caso contrário, null, inclusive para chamadas curtas demais para resumir, chamadas sem transcrição e agentes com a gravação desativada. O resumo é gerado na mesma etapa pós-chamada que a extração, portanto a configuração Aguardar dados extraídos antes de enviar o webhook de término da chamada (extraction_blocks_completion) determina se este evento aguarda por ele: em um modo de espera, o resumo fica pronto antes do envio deste evento (o próprio resumo recebe no máximo 30 segundos dentro do orçamento de 75 segundos da etapa); quando o evento não aguarda (never ou uma transferência em except_transfers), este campo é null e o resumo não é reenviado em call.data_extracted, portanto leia-o posteriormente em summary no objeto Call. Também disponível na página da chamada no dashboard
extracted_dataobjeto | nuloResultado de extração estruturada com status, fields, evidence, verification, field_reasons e schema_version. Cada campo não nulo possui a citação exata verificada estruturalmente (no máximo 1.000 caracteres), além da função de quem falou e do índice do turno; citações mais longas retornadas pelo modelo são rejeitadas em vez de truncadas. A evidência é null quando o campo é null. verified significa que cada candidato recebeu exatamente um veredito independente válido. unavailable também abrange saídas malformadas ou parciais do verificador; vereditos parciais válidos ainda se aplicam, enquanto candidatos sem um veredito válido são anulados. status é completed, failed, exhausted, skipped ou skipped_recording_disabled; null quando o agente não tinha campos de extração
steps_tracearraySomente em chamadas cujo agente tenha etapas ou transferências. A ordem em que a chamada passou por elas: cada entrada tem t_ms (milissegundos desde o início da chamada), agent (a chave do agente, a mais seu ID), step (o ID da etapa ou null em uma transferência para um agente sem etapas) e via (start, model, condition, on_enter ou handoff). Contém apenas IDs e horários, portanto é mantido mesmo quando a gravação está desativada
collected_valuesobjetoSomente em chamadas cujo agente tenha etapas ou transferências. Os valores que o agente coletou em suas etapas, indexados pelo nome do campo de coleta, além de <tool>_result para o resultado de on_enter_tool de cada etapa. Omitido quando o conteúdo da chamada não é retido (gravação desativada)

Motivos de encerramento

ValorSignificado
user_hangupA parte remota desligou primeiro
ai_hangupA IA encerrou a chamada deliberadamente
ai_transferA IA transferiu a chamada; transfer_number está definido
ai_warm_transferA IA concluiu uma transferência assistida
voicemail_hangupA caixa postal foi detectada e a chamada foi encerrada conforme sua voicemail_action
max_durationA chamada atingiu o limite máximo de duração: o limite do agente, o limite da própria chamada ou o limite de 6 horas da plataforma. Uma chamada que a IA ou quem liga encerra após o aviso de limite de tempo mantém ai_hangup ou user_hangup
supersededA sessão foi substituída por uma mais recente
inbound_config_webhook_failedChamada recebida para um número no modo webhook: o webhook de configuração não retornou uma configuração utilizável, portanto a chamada foi encerrada antes que um agente atendesse. status é failed e a chamada não é cobrada
unknownNão foi possível determinar o motivo do encerramento

Formato da transcrição

Cada entrada em transcripts corresponde a um turno de conversa. Os papéis são user (fala de quem liga), model (fala do agente e chamadas de ferramenta), tool (resultados de ferramenta) e system (eventos da chamada, como mudanças 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"] }
    }
  }
]
CampoTipoDescrição
rolestringuser, model, tool ou system
content_typestringtext/plain para fala; application/json para chamadas de ferramenta, resultados de ferramenta e eventos do sistema
contentstring | objectTexto da fala ou o objeto estruturado mostrado acima. Chamadas de ferramenta: {"tool_call": name, "arguments": {…}}. Resultados de ferramenta: {"tool_name": name, "response": {…}}
start_ms, end_msintegerDeslocamentos desde o início da chamada, em ms. Presentes quando a temporização do áudio é conhecida
ttfa_msintegerTempo até o primeiro áudio de um turno model, quando medido
audio_url, audio_urlsstring / arrayURLs assinadas com expiração para o áudio do turno, quando gravado por turno

Para o histórico de turnos totalmente estruturado (com marcadores de interrupção, prompts de confirmação e posições brutas), use GET /v1/calls/{call_id}/history.

Diferenças da carga útil legada

O envelope do webhook legado de URL única é {"type": "telephony.complete" | "web.complete", "data": {…}}, sem event_id, e seus data diferem da carga útil do endpoint:

A carga útil de conclusão legada também inclui agent_id e agent_name.

  • O array de turnos fica em history, não em transcripts (mesmo esquema de turnos mostrado acima).
  • O conjunto de campos é o relatório bruto de fim de chamada e pode incluir campos internos adicionais além da tabela acima — trate campos desconhecidos como informativos.
  • Chamadas web (direction: "web") omitem from_number / to_number e adicionam origin_domain.
  • As chamadas de teste de microfone do Builder são reportadas como telephony.complete no caminho legado (o sistema de endpoint as mapeia para web.complete).
  • Coordenação de transferência: quando uma chamada termina em uma transferência, o webhook legado é chamado de forma síncrona e pode responder {"transfer_ready": false} para sinalizar que o destino da transferência não está pronto. Qualquer outra resposta (ou nenhum webhook legado) permite que a transferência prossiga. As entregas do endpoint nunca são consultadas para isso.

Exemplo de manipulador

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 comuns

Integração com CRM

Armazene a transcrição e a URL da gravação de cada chamada junto aos registros de clientes.

Análises

Envie transcrições em streaming para um pipeline de modelagem de tópicos, extração de sinais de CSAT ou monitoramento da taxa de transferências.

Revisão de qualidade

Abra chamadas em uma ferramenta de garantia de qualidade para revisão humana ou processe-as no seu próprio modelo de avaliação.

Notificações

Acione um colega humano em caso de transferência ou falha.


Relacionados