ThunderPhone 2.0 jest już dostępny.Uruchom samodzielnie, od 2¢/min.Przeczytaj komunikat

Webhooks

telephony.complete / web.complete

Nieblokujący webhook dostarczany po zakończeniu połączenia, zawierający transkrypcję, URL nagrania i metryki.

Zdarzenie zakończenia jest wywoływane po zakończeniu każdego połączenia — telefonii przychodzącej, telefonii wychodzącej, połączenia internetowego lub połączenia testowego (sesji mikrofonu w kreatorze). Jest nieblokujące: odpowiedz dowolnym kodem 2xx.

Zdarzenie jest dostarczane obiema ścieżkami:

Treść żądania (dostarczenia do endpointu)

{
  "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"
}
PoleTypOpis
call_idintegerStałe we wszystkich zdarzeniach dla tego połączenia
agent_idinteger | nullAgent, który obsłużył połączenie, jeśli został przypisany
agent_namestring | nullAgent, który obsłużył połączenie, jeśli został przypisany
directionstringinbound, outbound, web, test. Historyczne ładunki mogą zawierać starsze wartości mic lub widget
from_number, to_numberstringE.164. Dla połączeń webowych i testowych from_number ma dosłowną wartość "web"
origin_domainstringTylko web/test — źródło strony, na której hostowano widżet (puste dla sesji mic)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullWyliczane na podstawie czasu rozpoczęcia i zakończenia
statusstringcompleted lub failed
end_reasonstringZobacz tabelę poniżej
product, voicestringKonfiguracja agenta obowiązująca w momencie połączenia
variablesobjectZmienne wejściowe zapisane w momencie rozpoczęcia połączenia
unresolved_variablesarrayNazwy zmiennych, do których odwołuje się konfiguracja połączenia, ale które nie zostały podane przy rozpoczęciu połączenia
transfer_numberstring | nullUstawiane, gdy połączenie zostało przekazane
recording_urlstring | nullWygasający podpisany URL; pobierz go niezwłocznie. null, gdy nagranie nie jest dostępne
recording_stopped_attimestamp | nullMoment zatrzymania nagrywania przed zakończeniem połączenia; nic po tym momencie nie znajduje się w nagraniu. null, gdy nagrywanie trwało do końca połączenia lub połączenie nie było nagrywane
recording_stopped_reasonstring | nullPowód wcześniejszego zatrzymania nagrywania. warm_transfer: nagrywanie zatrzymuje się tuż przed tym, jak transfer konsultowany połączy się z numerem docelowym, i pozostaje wyłączone, nawet jeśli przekazanie się nie powiedzie, a rozmówca wróci do agenta. Inne wartości traktuj jako informacyjne; mogą zostać dodane kolejne. null, gdy recording_stopped_at ma wartość null
billable_minutesnumberMinuty rozliczeniowe, zaokrąglone do najbliższej ćwierć minuty (co 15 sekund, minimum 0.25). Połączenia bezpośrednio z pocztą głosową nadal zgłaszają tutaj rzeczywistą liczbę naliczonych minut, ale opłata jest ograniczona do jednej minuty według stawki planu.
billing_total_centsintegerCenty USD
transcriptsarrayWpisy transkrypcji dla każdej tury; mogą być puste, gdy transkrypcja jest niedostępna
summarystring | nullPodsumowanie połączenia prostym językiem (od dwóch do czterech zdań prozy, maksymalnie 2,000 znaków, napisane w języku połączenia: podstawowym języku agenta, chyba że połączenie go zastąpiło), gdy agent ma włączoną opcję Generowanie podsumowania rozmowy w sekcji Dane do zebrania w kreatorze agenta (call_summary_enabled w obiekcie Agent) i generowanie się powiodło; w przeciwnym razie null, w tym dla połączeń zbyt krótkich do podsumowania, połączeń bez transkrypcji oraz agentów z wyłączonym nagrywaniem. Podsumowanie jest generowane w tym samym kroku po połączeniu co ekstrakcja, dlatego ustawienie Czekaj na wyodrębnione dane przed wysłaniem webhooka zakończenia połączenia (extraction_blocks_completion) określa, czy to zdarzenie na nie czeka: w trybie oczekiwania podsumowanie jest gotowe przed wysłaniem tego zdarzenia (samo podsumowanie ma maksymalnie 30 sekund w ramach 75-sekundowego limitu kroku); gdy zdarzenie nie czeka (never lub przekazanie w ramach except_transfers), to pole ma wartość null, a podsumowanie nie jest wysyłane ponownie w call.data_extracted, więc odczytaj je później z pola summary w obiekcie Call. Jest również dostępne na stronie połączenia w panelu
extracted_dataobject | nullWynik ustrukturyzowanej ekstrakcji z polami status, fields, evidence, verification, field_reasons i schema_version. Każde pole inne niż null zawiera dokładny cytat sprawdzony strukturalnie (maksymalnie 1,000 znaków), a także rolę mówcy i indeks tury; dłuższe cytaty zwrócone przez model są odrzucane zamiast skracane. Dowód ma wartość null, gdy pole ma wartość null. verified oznacza, że każdy kandydat otrzymał dokładnie jeden prawidłowy niezależny werdykt. unavailable obejmuje również nieprawidłowe lub częściowe dane wyjściowe weryfikatora; prawidłowe częściowe werdykty nadal są stosowane, natomiast kandydaci bez jednego prawidłowego werdyktu otrzymują wartość null. status ma wartość completed, failed, exhausted, skipped lub skipped_recording_disabled; null, gdy agent nie miał pól ekstrakcji
steps_tracearrayTylko w połączeniach, których agent ma kroki lub przekazania. Kolejność, w jakiej połączenie przez nie przechodziło: każdy wpis zawiera t_ms (milisekundy od rozpoczęcia połączenia), agent (klucz agenta, a oraz jego ID), step (ID kroku lub null przy przekazaniu do agenta bez kroków) oraz via (start, model, condition, on_enter lub handoff). Zawiera tylko identyfikatory i czasy, dlatego jest zachowywane nawet przy wyłączonym nagrywaniu
collected_valuesobjectTylko w połączeniach, których agent ma kroki lub przekazania. Wartości zebrane przez agenta w jego krokach, z kluczem będącym nazwą pola zbierania, oraz <tool>_result dla wyniku on_enter_tool każdego kroku. Pomijane, gdy treść połączenia nie jest przechowywana (nagrywanie wyłączone)

Powody zakończenia

WartośćZnaczenie
user_hangupZdalna strona rozłączyła się jako pierwsza
ai_hangupAI celowo zakończyła połączenie
ai_transferAI przekazała połączenie; ustawiono transfer_number
ai_warm_transferAI zakończyła transfer z konsultacją
voicemail_hangupWykryto pocztę głosową, a połączenie zakończono zgodnie z ustawieniem voicemail_action
max_durationPołączenie osiągnęło maksymalny limit czasu trwania: limit agenta, własny limit połączenia lub 6-godzinny limit platformy. Połączenie zakończone przez AI lub rozmówcę po ostrzeżeniu o limicie czasu zachowuje wartość ai_hangup lub user_hangup
supersededSesja została zastąpiona nowszą
inbound_config_webhook_failedPołączenie przychodzące na numer w trybie webhooka: webhook konfiguracji nie zwrócił użytecznej konfiguracji, więc połączenie zakończono, zanim agent odebrał. status ma wartość failed, a połączenie nie jest rozliczane
unknownNie można było ustalić przyczyny zakończenia

Format transkrypcji

Każdy wpis w transcripts to jedna tura rozmowy. Role to user (wypowiedź rozmówcy), model (wypowiedź agenta oraz wywołania narzędzi), tool (wyniki narzędzi) i system (zdarzenia połączenia, takie jak zmiany języka).

[
  {
    "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"] }
    }
  }
]
PoleTypOpis
rolestringuser, model, tool lub system
content_typestringtext/plain dla mowy; application/json dla wywołań narzędzi, wyników narzędzi i zdarzeń systemowych
contentstring | objectTekst mowy lub obiekt strukturalny przedstawiony powyżej. Wywołania narzędzi: {"tool_call": name, "arguments": {…}}. Wyniki narzędzi: {"tool_name": name, "response": {…}}
start_ms, end_msintegerPrzesunięcia od rozpoczęcia połączenia w ms. Obecne, gdy znane jest czasowanie audio
ttfa_msintegerCzas do pierwszego audio dla tury model, gdy jest mierzony
audio_url, audio_urlsstring / arrayWygasające podpisane adresy URL audio dla tury, gdy audio jest nagrywane dla każdej tury

Aby uzyskać w pełni ustrukturyzowaną historię tur (ze znacznikami przerwań, monitami potwierdzającymi i surowymi pozycjami), użyj GET /v1/calls/{call_id}/history.

Różnice w starszym ładunku

Starsza koperta webhooka z jednym adresem URL ma postać {"type": "telephony.complete" | "web.complete", "data": {…}} i nie zawiera event_id, a jej data różni się od ładunku punktu końcowego:

Starszy ładunek zakończenia zawiera również agent_id i agent_name.

  • Tablica tur znajduje się w history, a nie w transcripts (ten sam schemat tury co powyżej).
  • Zestaw pól jest surowym raportem końca połączenia i może zawierać dodatkowe pola wewnętrzne wykraczające poza powyższą tabelę — traktuj nieznane pola jako informacyjne.
  • Połączenia internetowe (direction: "web") pomijają from_number / to_number i dodają origin_domain.
  • Połączenia testowe mikrofonu w kreatorze są raportowane jako telephony.complete w starszej ścieżce (system punktu końcowego mapuje je na web.complete).
  • Koordynacja przekazania: gdy połączenie kończy się przekazaniem, starszy webhook jest wywoływany synchronicznie i może odpowiedzieć {"transfer_ready": false}, aby zasygnalizować, że docelowy odbiorca przekazania nie jest gotowy. Każda inna odpowiedź (lub brak starszego webhooka) pozwala na kontynuowanie przekazania. Dostawy do punktu końcowego nigdy nie są w tym celu sprawdzane.

Przykładowy handler

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

Typowe zastosowania

Integracja z CRM

Zapisuj transkrypcję każdego połączenia i adres URL nagrania wraz z danymi klientów.

Analityka

Przesyłaj strumieniowo transkrypcje do potoku w celu modelowania tematów, wyodrębniania sygnałów CSAT lub monitorowania wskaźnika przekazań.

Kontrola jakości

Otwieraj połączenia w narzędziu QA do weryfikacji przez człowieka lub przetwarzaj je przez własny model oceny.

Powiadomienia

Powiadamiaj członka zespołu przy przekazaniu lub niepowodzeniu.


Powiązane