ThunderPhone 2.0 je tady.Začnete bez obchodníka, od 2 ¢/min.Přečíst oznámení

Webhooks

telephony.complete / web.complete

Neblokující webhook doručený po ukončení hovoru s přepisem, URL záznamu a metrikami.

Událost dokončení se vyvolá po skončení každého hovoru — příchozího telefonního hovoru, odchozího telefonního hovoru, webového hovoru nebo testovacího hovoru (relace mikrofonu v editoru). Je neblokující: odpovězte libovolným kódem 2xx.

Událost se doručuje oběma cestami:

Datový obsah požadavku (doručení na koncový bod)

{
  "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"
}
PoleTypPopis
call_idintegerNeměnné ve všech událostech pro tento hovor
directionstringinbound, outbound, web, test. Historické datové obsahy mohou obsahovat starší hodnoty mic nebo widget
from_number, to_numberstringE.164. Pro webové a testovací hovory je from_number doslovně "web"
origin_domainstringPouze web/test — původ stránky, na které byl widget hostován (pro relace mikrofonu prázdné)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullOdvozeno z času začátku a konce
statusstringcompleted nebo failed
end_reasonstringViz tabulka níže
product, voicestringKonfigurace agenta platná v době hovoru
transfer_numberstring | nullNastaví se při přepojení hovoru
recording_urlstring | nullPodepsaná adresa URL s omezenou platností; stáhněte ji neprodleně. null, pokud není k dispozici záznam hovoru
billable_minutesnumberÚčtované minuty zaokrouhlené na nejbližší čtvrt minuty (15sekundové intervaly, minimum 0.25). Hovory přímo do hlasové schránky zde stále uvádějí skutečně naměřené minuty, ale poplatek je omezen na jednu minutu podle sazby tarifu.
billing_total_centsintegerCenty USD
transcriptsarrayPoložky přepisu pro jednotlivé repliky; při nedostupném přepisu může být prázdné

Důvody ukončení

HodnotaVýznam
user_hangupProtistrana zavěsila jako první
ai_hangupAgent AI hovor záměrně ukončil
ai_transferAgent AI hovor přepojil; je nastaveno transfer_number
ai_warm_transferAgent AI dokončil teplé (asistované) přepojení
voicemail_hangupByla rozpoznána hlasová schránka a hovor byl ukončen podle vašeho voicemail_action
max_durationHovor dosáhl limitu maximální délky
supersededRelace byla nahrazena novější relací
unknownDůvod ukončení nebylo možné určit

Formát přepisu

Každá položka v transcripts představuje jeden konverzační tah. Role jsou user (řeč volajícího), model (řeč agenta a volání nástrojů), tool (výsledky nástrojů) a system (události hovoru, například změny jazyka).

[
  {
    "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"] }
    }
  }
]
PoleTypPopis
roleřetězecuser, model, tool nebo system
content_typeřetězectext/plain pro řeč; application/json pro volání nástrojů, výsledky nástrojů a systémové události
contentřetězec | objektText řeči nebo strukturovaný objekt uvedený výše. Volání nástrojů: {"tool_call": name, "arguments": {…}}. Výsledky nástrojů: {"tool_name": name, "response": {…}}
start_ms, end_mscelé čísloPosuny od začátku hovoru v ms. Jsou přítomné, když je známé časování zvuku
ttfa_mscelé čísloDoba do prvního zvuku pro tah model, pokud je měřena
audio_url, audio_urlsřetězec / poleURL s dočasným podpisem pro zvuk tahu, pokud je zvuk zaznamenáván pro jednotlivé tahy

Pro plně strukturovanou historii tahů (se značkami přerušení, výzvami k potvrzení a nezpracovanými pozicemi) použijte GET /v1/calls/{call_id}/history.

Rozdíly staršího payloadu

Starší webhookový obal s jedinou adresou URL je {"type": "telephony.complete" | "web.complete", "data": {…}} bez event_id a jeho data se liší od payloadu koncového bodu:

  • Pole tahů je v history, nikoli v transcripts (stejné schéma tahů jako výše).
  • Sada polí představuje nezpracovaný report o ukončení hovoru a může obsahovat další interní pole nad rámec výše uvedené tabulky — s neznámými poli zacházejte jako s informativními.
  • Webové hovory (direction: "web") vynechávají from_number / to_number a přidávají origin_domain.
  • Testovací hovory mikrofonu v Builderu se ve starší cestě hlásí jako telephony.complete (systém koncového bodu je mapuje na web.complete).
  • Koordinace přepojení: když hovor skončí přepojením, starší webhook se volá synchronně a může vrátit {"transfer_ready": false}, aby signalizoval, že cíl přepojení není připraven. Jakákoli jiná odpověď (nebo žádný starší webhook) umožní pokračování přepojení. Doručení koncovému bodu se k tomuto účelu nikdy nekonzultuje.

Příklad obslužné funkce

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

Běžné případy použití

Integrace CRM

Ukládejte přepis a adresu URL nahrávky každého hovoru spolu se záznamy o zákaznících.

Analytika

Streamujte přepisy do pipeline pro modelování témat, extrakci signálů CSAT nebo sledování míry přepojení.

Kontrola kvality

Otevírejte hovory v nástroji QA pro lidskou kontrolu nebo je spouštějte prostřednictvím vlastního vyhodnocovacího modelu.

Oznámení

Při přepojení nebo selhání upozorněte lidského člena týmu.


Související