ThunderPhone 2.0 је стигао.Почните самостално, већ од 2 ¢/мин.Прочитајте објаву

Webhooks

telephony.complete / web.complete

Неблокирајући вебхук који се испоручује када се позив заврши, са транскриптом, 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",
    "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"
}
ПољеТипОпис
call_idintegerСтабилно у сваком догађају за овај позив
agent_idinteger | nullАгент који је обрадио позив, ако је био додељен
agent_namestring | nullАгент који је обрадио позив, ако је био додељен
directionstringinbound, outbound, web, test. Историјска тела могу садржати застареле вредности mic или widget
from_number, to_numberstringE.164. from_number је дословно "web" за веб-позиве и тест-позиве
origin_domainstringСамо за веб/тестове — извор странице на којој је смештен виџет (празно за сесије микрофона)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullИзведено из времена почетка/завршетка
statusstringcompleted или failed
end_reasonstringПогледајте табелу у наставку
product, voicestringКонфигурација агента активна у време позива
transfer_numberstring | nullПостављено када је позив преусмерен
recording_urlstring | nullПотписани URL који истиче; преузмите га одмах. null када артефакт снимка није доступан
billable_minutesnumberНаплаћени минути, заокружени на најближу четвртину минута (инкременти од 15 секунди, минимум 0.25). Позиви који иду директно на говорну пошту и даље овде приказују стварно измерене минуте, али је наплата ограничена на један минут по тарифној стопи плана.
billing_total_centsintegerАмерички центи
transcriptsarrayУноси транскрипта за сваку говорну размену; могу бити празни када транскрипт није доступан

Разлози завршетка

ВредностЗначење
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цео бројПомераји од почетка позива, у мс. Присутно када је познато аудио-време
ttfa_msцео бројВреме до првог звука за model потез, када је измерено
audio_url, audio_urlsниска / низПотписани УРЛ-ови са истеком за аудио записа потеза, када се снима по потезу

За потпуно структурисану историју потеза (са ознакама прекида, упитима за потврду и сировим позицијама), користите GET /v1/calls/{call_id}/history.

Разлике у застарелом садржају

Застарела омотница веб-хука са једним УРЛ-ом је {"type": "telephony.complete" | "web.complete", "data": {…}} без event_id, а њен data се разликује од садржаја крајње тачке:

Застарели садржај о завршетку такође садржи agent_id и agent_name.

  • Низ потеза је у оквиру history, а не transcripts (иста шема потеза као изнад).
  • Скуп поља је сиров извештај на крају позива и може да садржи додатна интерна поља поред табеле изнад — непозната поља третирајте као информативна.
  • Веб позиви (direction: "web") изостављају from_number / to_number и додају origin_domain.
  • Позиви за тестирање микрофона у градитељу пријављују се као telephony.complete на застарелој путањи (систем крајње тачке их мапира на web.complete).
  • Координација преноса: када се позив заврши преносом, застарели веб-хук се позива синхроно и може да врати {"transfer_ready": false} како би означио да циљ преноса није спреман. Сваки други одговор (или одсуство застарелог веб-хука) омогућава наставак преноса. Испоруке крајњој тачки се за ово никада не проверавају.

Пример обрађивача

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 сигнала или праћење стопе преноса.

Преглед квалитета

Отворите позиве у QA алату за људски преглед или их покрените кроз сопствени модел за евалуацију.

Обавештења

Обавестите члана тима приликом преноса / неуспеха.


Повезано