ThunderPhone 2.0 вече е тук.Започнете самостоятелно — от 2 цента/мин.Прочетете съобщението

Webhooks

telephony.complete / web.complete

Неблокиращ webhook, изпращан при приключване на разговор, с транскрипция, URL адрес на записа и показатели.

Събитие за завършване се задейства след края на всяко обаждане — входящо телефонно обаждане, изходящо телефонно обаждане, уеб обаждане или тестово обаждане (микрофонна сесия в конструктора). То е неблокиращо: отговорете с който и да е 2xx.

Събитието се доставя по двата пътя:

Тяло на заявката (доставки до крайни точки)

{
  "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_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,
    "unresolved_variables": ["campaign_owner"],
    "variables": {"campaign_name": "Spring renewals"},
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
ПолеТипОписание
call_idintegerПостоянен във всяко събитие за това обаждане
agent_idinteger | nullАгентът, обработил обаждането, когато е бил зададен
agent_namestring | nullАгентът, обработил обаждането, когато е бил зададен
directionstringinbound, outbound, web, test. Историческите тела на заявки може да съдържат остарелите стойности mic или widget
from_number, to_numberstringE.164. from_number е буквалната стойност "web" за уеб обаждания и тестови обаждания
origin_domainstringСамо за уеб/тест — произходът на страницата, която хоства уиджета (празно за mic сесии)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullИзчислява се от началото и края
statusstringcompleted или failed
end_reasonstringВижте таблицата по-долу
product, voicestringКонфигурацията на агента, използвана по време на обаждането
variablesobjectВходни променливи, запазени като моментна снимка при началото на обаждането
unresolved_variablesarrayИмена на променливи, посочени в конфигурацията на обаждането, но непредоставени при започването му
transfer_numberstring | nullЗадава се при прехвърляне на обаждането
recording_urlstring | nullПодписан URL с изтичащ срок; изтеглете го своевременно. null, когато няма наличен запис
billable_minutesnumberТаксувани минути, закръглени до най-близката четвърт минута (интервали от 15 секунди, минимум 0.25). Обажданията, насочени директно към гласова поща, все пак отчитат тук действителните си измерени минути, но таксата е ограничена до една минута по тарифата на плана.
billing_total_centsintegerЦентове в USD
transcriptsarrayЗаписи от транскрипцията за всеки ход; може да е празно, когато транскрипция не е налична
extracted_dataobject | nullРезултат от структурирано извличане със status, fields, evidence, verification, field_reasons и schema_version. Всяко поле, различно от null, съдържа точния структурно проверен цитат (до 1 000 знака), както и ролята на говорещия и индекса на хода; по-дългите цитати, върнати от модела, се отхвърлят, вместо да бъдат съкращавани. Evidence е null, когато полето е null. verified означава, че всеки кандидат е получил точно една валидна независима оценка. unavailable обхваща също неправилно форматиран или частичен изход от проверяващия модул; валидните частични оценки все пак се прилагат, а кандидатите без една валидна оценка се задават на null. status е completed, failed, exhausted, skipped или skipped_recording_disabled; null, когато агентът няма полета за извличане

Причини за приключване

СтойностЗначение
user_hangupОтдалечената страна първа е прекратила разговора
ai_hangupAI умишлено е прекратил обаждането
ai_transferAI е прехвърлил обаждането; transfer_number е зададен
ai_warm_transferAI е завършил топло прехвърляне с участник
voicemail_hangupОткрита е гласова поща и обаждането е прекратено според вашия voicemail_action
max_durationОбаждането е достигнало максималното ограничение за продължителност
supersededСесията е заменена с по-нова
unknownПричината за приключване не можа да бъде определена

Формат на транскрипта

Всеки запис в transcripts е един разговорен ход. Ролите са user (реч на обаждащия се), model (реч на агента и извиквания на инструменти), tool (резултати от инструменти) и system (събития на разговора, като превключвания на езика).

[
  {
    "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"] }
    }
  }
]
ПолеТипОписание
roleнизuser, model, tool или system
content_typeнизtext/plain за реч; application/json за извиквания на инструменти, резултати от инструменти и системни събития
contentниз | обектТекст на речта или структуриран обект като показания по-горе. Извиквания на инструменти: {"tool_call": name, "arguments": {…}}. Резултати от инструменти: {"tool_name": name, "response": {…}}
start_ms, end_msцяло числоОтместване от началото на разговора в ms. Налице, когато е известно времето на аудиото
ttfa_msцяло числоВреме до първото аудио за ход на model, когато е измерено
audio_url, audio_urlsниз / масивИзтичащи подписани URL адреси за аудиото на хода, когато се записва за всеки ход

За напълно структурираната история на ходовете (с маркери за прекъсване, подкани за потвърждение и необработени позиции) използвайте GET /v1/calls/{call_id}/history.

Разлики в остарелия payload

Остарелият webhook контейнер с един URL е {"type": "telephony.complete" | "web.complete", "data": {…}} без event_id, а неговият data се различава от payload-а на endpoint-а:

Остарелият payload при завършване включва също agent_id и agent_name.

  • Масивът с ходове е в history, а не в transcripts (същата схема на ходовете като по-горе).
  • Наборът от полета е необработеният отчет в края на разговора и може да включва допълнителни вътрешни полета извън таблицата по-горе — третирайте непознатите полета като информативни.
  • Уеб разговорите (direction: "web") пропускат from_number / to_number и добавят origin_domain.
  • Тестовите разговори с микрофон в Builder се отчитат като telephony.complete по остарелия път (системата на endpoint-а ги съпоставя с web.complete).
  • Координация на прехвърлянето: когато разговор завърши с прехвърляне, остарелият webhook се извиква синхронно и може да върне {"transfer_ready": false}, за да сигнализира, че целта на прехвърлянето не е готова. Всеки друг отговор (или липса на остарял webhook) позволява прехвърлянето да продължи. Доставките към endpoint никога не се проверяват за това.

Примерен обработчик

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

Често срещани случаи на употреба

Интеграция с CRM

Съхранявайте транскрипта и URL адреса на записа на всяко обаждане заедно с данните за клиентите си.

Анализи

Предавайте транскрипти към конвейер за моделиране на теми, извличане на CSAT сигнали или наблюдение на процента прехвърляния.

Преглед на качеството

Отваряйте обаждания в инструмент за осигуряване на качеството за преглед от човек или ги обработвайте чрез собствен модел за оценяване.

Известия

Уведомявайте член на екипа при прехвърляне / неуспех.


Свързано