ThunderPhone 2.0 je tady.Spustíte sami, 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 nahrávky a metrikami.

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

Událost je doručena oběma cestami:

Datová část požadavku (doručení 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"
}
PoleTypPopis
call_idcelé čísloStabilní napříč všemi událostmi tohoto hovoru
agent_idcelé číslo | nullAgent, který hovor vyřídil, pokud byl přiřazen
agent_nameřetězec | nullAgent, který hovor vyřídil, pokud byl přiřazen
directionřetězecinbound, outbound, web, test. Historické datové pakety mohou obsahovat starší hodnoty mic nebo widget
from_number, to_numberřetězecE.164. from_number má pro webové a testovací hovory doslovnou hodnotu "web"
origin_domainřetězecPouze pro web/test — původ stránky, na které byl widget hostován (pro relace mic je prázdný)
start_time, end_timečasové razítkoISO 8601 UTC
duration_secondscelé číslo | nullOdvozeno z času začátku/konce
statusřetězeccompleted nebo failed
end_reasonřetězecViz tabulku níže
product, voiceřetězecKonfigurace agenta platná v době hovoru
variablesobjektVstupní proměnné zachycené při zahájení hovoru
unresolved_variablespoleNázvy proměnných, na které odkazuje konfigurace hovoru, ale které nebyly dodány při zahájení hovoru
transfer_numberřetězec | nullNastaveno při přepojení hovoru
recording_urlřetězec | nullPodepsaná adresa URL s omezenou platností; stáhněte ji neprodleně. null, když není k dispozici žádný záznam hovoru
recording_stopped_atčasové razítko | nullOkamžik, kdy bylo nahrávání zastaveno před koncem hovoru; nic po tomto okamžiku v nahrávce není. null, když nahrávání pokračovalo až do konce hovoru nebo hovor nebyl nahráván
recording_stopped_reasonřetězec | nullDůvod předčasného zastavení nahrávání. warm_transfer: nahrávání se zastaví těsně předtím, než teplé přepojení zavolá cílovou stranu, a zůstane vypnuté i v případě, že přepojení selže a volající se vrátí k agentovi. Ostatní hodnoty považujte za informativní; mohou být přidány další. null, když má recording_stopped_at hodnotu null
billable_minutesčísloFakturované minuty zaokrouhlené na nejbližší čtvrtminutu (v krocích po 15 sekundách, 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_centscelé čísloCenty USD
transcriptspolePoložky přepisu pro jednotlivé repliky; může být prázdné, když přepis není k dispozici
summaryřetězec | nullShrnutí po hovoru v běžném jazyce (dvě až čtyři věty prózy, maximálně 2 000 znaků, napsané v jazyce hovoru: primárním jazyce agenta, pokud ho hovor nepřepsal), když má agent v nástroji pro tvorbu agenta v části Data k získání zapnutou možnost Generovat shrnutí hovoru (call_summary_enabled u Agenta) a generování proběhlo úspěšně; jinak null, včetně hovorů příliš krátkých na shrnutí, hovorů bez přepisu a agentů s vypnutým nahráváním. Shrnutí se generuje ve stejném kroku po hovoru jako extrakce, takže nastavení Čekat na extrahovaná data před odesláním webhooku o ukončení hovoru (extraction_blocks_completion) určuje, zda na něj tato událost čeká: v režimu čekání je shrnutí připraveno před odesláním této události (samotné shrnutí má v rámci 75sekundového rozpočtu kroku nejvýše 30 sekund); když událost nečeká (never nebo přepojení v režimu except_transfers), má toto pole hodnotu null a shrnutí se znovu neodesílá v call.data_extracted, proto jej později načtěte z summary u objektu hovoru. K dispozici je také na stránce hovoru v dashboardu
extracted_dataobjekt | nullVýsledek strukturované extrakce s status, fields, evidence, verification, field_reasons a schema_version. Každé pole jiné než null obsahuje přesnou strukturálně ověřenou citaci (maximálně 1 000 znaků) spolu s rolí mluvčího a indexem repliky; delší citace vrácené modelem jsou místo zkrácení zamítnuty. Důkaz má hodnotu null, když má pole hodnotu null. verified znamená, že každý kandidát získal přesně jeden platný nezávislý verdikt. unavailable zahrnuje také chybný nebo neúplný výstup ověřovatele; platné částečné verdikty se stále použijí, zatímco kandidáti bez jednoho platného verdiktu získají hodnotu null. status je completed, failed, exhausted, skipped nebo skipped_recording_disabled; null, když agent neměl žádná pole pro extrakci
steps_tracepolePouze u hovorů, jejichž agent má kroky nebo předání. Pořadí, v jakém jimi hovor procházel: každá položka obsahuje t_ms (milisekundy od začátku hovoru), agent (klíč agenta, a plus jeho ID), step (ID kroku nebo null při předání agentovi bez kroků) a via (start, model, condition, on_enter nebo handoff). Obsahuje pouze ID a časy, proto se uchovává i při vypnutém nahrávání
collected_valuesobjektPouze u hovorů, jejichž agent má kroky nebo předání. Hodnoty, které agent získal ve svých krocích, klíčované názvem pole pro získání, a také <tool>_result pro výsledek on_enter_tool každého kroku. Vynecháno, když se obsah hovoru neuchovává (nahrávání je vypnuté)

Důvody ukončení

HodnotaVýznam
user_hangupVolaná strana zavěsila jako první
ai_hangupAI hovor záměrně ukončila
ai_transferAI hovor přepojila; je nastaveno transfer_number
ai_warm_transferAI dokončila asistované přepojení
voicemail_hangupByla rozpoznána hlasová schránka a hovor byl ukončen podle vašeho voicemail_action
max_durationHovor dosáhl maximálního limitu délky: limitu agenta, vlastního limitu hovoru nebo šestihodinového limitu platformy. Hovor, který AI nebo volající ukončí po upozornění na časový limit, si zachová ai_hangup nebo user_hangup
supersededRelace byla nahrazena novější
inbound_config_webhook_failedPříchozí hovor na číslo v režimu webhooku: konfigurační webhook nevrátil použitelnou konfiguraci, proto byl hovor ukončen předtím, než agent odpověděl. status je failed a hovor není účtován
unknownDůvod ukončení se nepodařilo určit

Formát přepisu

Každá položka v transcripts představuje jeden tah konverzace. 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 přepnutí 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 uvedeny, když je známé časování audia
ttfa_mscelé čísloDoba do prvního audia pro tah model, pokud je měřena
audio_url, audio_urlsřetězec / poleURL s podpisem a omezenou platností pro audio daného tahu, pokud se audio nahrává 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 ve starším datovém formátu

Starší obálka webhooku s jednou URL je {"type": "telephony.complete" | "web.complete", "data": {…}} bez event_id a její data se liší od datového formátu endpointu:

Starší datový formát dokončení rovněž obsahuje agent_id a agent_name.

  • Pole tahů je v history, nikoli v transcripts (stejné schéma tahů jako výše).
  • Sada polí představuje nezpracovaný report po ukončení hovoru a může zahrnovat další interní pole nad rámec výše uvedené tabulky — neznámá pole považujte za informativní.
  • Webové hovory (direction: "web") vynechávají from_number / to_number a přidávají origin_domain.
  • Testovací hovory mikrofonu v nástroji Builder se ve starší cestě hlásí jako telephony.complete (systém endpointu je mapuje na web.complete).
  • Koordinace přepojení: když hovor končí přepojením, starší webhook je volán synchronně a může odpovědět {"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í endpointu se pro tento účel nikdy nekontroluje.

Příklad obslužné rutiny

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 společně se záznamy zákazníků.

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 kontrolu člověkem nebo je nechte vyhodnotit vlastním hodnoticím modelem.

Oznámení

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


Související