telephony.complete / web.complete
Estämätön webhook, joka toimitetaan puhelun päättyessä ja sisältää litteraatin, tallenteen URL-osoitteen ja mittarit.
Valmistumistapahtuma käynnistyy jokaisen puhelun päätyttyä — saapuva puhelinliikenne, lähtevä puhelinliikenne, verkkopuhelu tai testipuhelu (rakentajan mikrofonisessio). Se on ei-estävä: vastaa millä tahansa 2xx-vastauksella.
Tapahtuma toimitetaan molempiin polkuihin:
- Webhook-päätepisteet vastaanottavat
telephony.complete-tapahtuman (puhelut) taiweb.complete-tapahtuman (verkkopuhelut ja rakentajan mikrofonitestipuhelut) alla dokumentoidulla vakaalla hyötykuormalla, toimituskohtaisellaevent_id:llä, 30 sekunnin aikakatkaisulla ja uudelleenyrityksillä enintään 24 tunnin ajan. - Vanha yhden URL-osoitteen webhook vastaanottaa yhden synkronisen yrityksen (10 sekunnin aikakatkaisu, ei uudelleenyrityksiä) hieman erilaisella hyötykuormalla — katso Vanhan hyötykuorman erot.
Pyynnön hyötykuorma (päätepistetoimitukset)
{
"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"
}| Kenttä | Tyyppi | Kuvaus |
|---|---|---|
call_id | integer | Vakaa kaikissa tämän puhelun tapahtumissa |
direction | string | inbound, outbound, web, test. Historialliset hyötykuormat voivat sisältää vanhat arvot mic tai widget |
from_number, to_number | string | E.164. from_number on kirjaimellisesti "web" verkkopuheluissa ja testipuheluissa |
origin_domain | string | Vain verkko/testi — sivun alkuperä, jolla widgettiä isännöitiin (tyhjä mikrofonisessioissa) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | Johdettu alku- ja loppuajasta |
status | string | completed tai failed |
end_reason | string | Katso alla oleva taulukko |
product, voice | string | Puhelun aikana käytössä ollut agentin määritys |
transfer_number | string | null | Asetetaan, kun puhelu siirrettiin |
recording_url | string | null | Vanheneva allekirjoitettu URL; lataa nopeasti. null, kun tallenneartefaktia ei ole saatavilla |
billable_minutes | number | Laskutettavat minuutit pyöristettynä lähimpään neljännesminuuttiin (15 sekunnin välein, vähintään 0.25). Suoraan vastaajaan menevät puhelut ilmoittavat silti todelliset mitatut minuuttinsa tässä, mutta veloitus on enintään yksi minuutti sopimushinnalla. |
billing_total_cents | integer | USD-sentit |
transcripts | array | Vuorokohtaiset transkriptiomerkinnät; voi olla tyhjä, kun transkriptiota ei ole saatavilla |
Lopetussyyt
| Arvo | Merkitys |
|---|---|
user_hangup | Etäosapuoli katkaisi ensin |
ai_hangup | Agentti lopetti puhelun tarkoituksellisesti |
ai_transfer | Agentti siirsi puhelun; transfer_number on asetettu |
ai_warm_transfer | Agentti suoritti lämpimän (avustetun) siirron |
voicemail_hangup | Vastaaja havaittiin ja puhelu päättyi voicemail_action-asetuksesi mukaisesti |
max_duration | Puhelu saavutti enimmäiskestorajan |
superseded | Istunto korvattiin uudemmalla |
unknown | Lopetussyytä ei voitu määrittää |
Transkriptimuoto
Jokainen transcripts-merkintä on yksi keskusteluvuoro. Roolit ovat
user (soittajan puhe), model (agentin puhe sekä työkalukutsut),
tool (työkalun tulokset) ja system (puhelutapahtumat, kuten kielen
vaihdot).
[
{
"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"] }
}
}
]| Kenttä | Tyyppi | Kuvaus |
|---|---|---|
role | merkkijono | user, model, tool tai system |
content_type | merkkijono | text/plain puheelle; application/json työkalukutsuille, työkalun tuloksille ja järjestelmätapahtumille |
content | merkkijono | objekti | Puheteksti tai yllä näytetty jäsennelty objekti. Työkalukutsut: {"tool_call": name, "arguments": {…}}. Työkalun tulokset: {"tool_name": name, "response": {…}} |
start_ms, end_ms | kokonaisluku | Siirtymät puhelun alusta millisekunteina. Sisältyvät, kun äänen ajoitus on tiedossa |
ttfa_ms | kokonaisluku | model-vuoron ensimmäiseen ääneen kuluva aika millisekunteina, kun se on mitattu |
audio_url, audio_urls | merkkijono / taulukko | Vanhenevat allekirjoitetut URL-osoitteet vuoron äänelle, kun ääni tallennetaan vuorokohtaisesti |
Käytä täysin jäsenneltyyn vuorohistoriaan (keskeytysmerkkeineen,
vahvistuskehotteineen ja raakapositioineen)
GET /v1/calls/{call_id}/history-päätepistettä.
Vanhan hyötykuorman erot
Vanhan yhden URL-osoitteen webhook-kuori on
{"type": "telephony.complete" | "web.complete", "data": {…}}, eikä siinä ole
event_id-kenttää. Sen data eroaa päätepisteen hyötykuormasta:
- Vuorotaulukko on
history-kentässä, eitranscripts-kentässä (sama vuorokaava kuin yllä). - Kenttäjoukko on puhelun päättymisen raaka raportti, ja se voi sisältää taulukossa esitettyjen kenttien lisäksi sisäisiä kenttiä — käsittele tuntemattomia kenttiä tiedoksi tarkoitettuina.
- Web-puhelut (
direction: "web") jättävät pois kentätfrom_number/to_numberja lisäävät kentänorigin_domain. - Builderin mikrofonitestipuhelut raportoidaan vanhassa polussa muodossa
telephony.complete(päätepistejärjestelmä yhdistää ne muotoonweb.complete). - Siirron koordinointi: kun puhelu päättyy siirtoon, vanhaa webhookia
kutsutaan synkronisesti, ja se voi vastata
{"transfer_ready": false}ilmoittaakseen, ettei siirron kohde ole valmis. Mikä tahansa muu vastaus (tai vanhan webhookin puuttuminen) sallii siirron jatkua. Päätepistetoimituksia ei koskaan tarkisteta tätä varten.
Esimerkkikäsittelijä
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}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 });
},
);Yleiset käyttötapaukset
Tallenna jokaisen puhelun transkriptio ja tallenteen URL-osoite asiakastietojesi yhteyteen.
Suoratoista transkriptiot työnkulkuun aiheiden mallintamista, CSAT-signaalien poimintaa tai siirtoasteen seurantaa varten.
Avaa puhelut QA-työkalussa ihmisen tarkistettaviksi tai suorita ne oman arviointimallisi läpi.
Ilmoita ihmistiimin jäsenelle siirrosta / epäonnistumisesta.