Open in
telephony.complete / web.complete
Estoton webhook, joka toimitetaan puhelun päättyessä ja sisältää transkriptin, tallenteen URL-osoitteen ja mittarit.
Valmistumistapahtuma käynnistyy jokaisen puhelun päätyttyä — saapuvan puhelun, lähtevän puhelun, verkkopuhelun tai testipuhelun (rakentajan mikrofonisessio) jälkeen. Se on ei-estävä: vastaa millä tahansa 2xx-tilakoodilla.
Tapahtuma toimitetaan molempia reittejä pitkin:
- Webhook-päätepisteet vastaanottavat
telephony.complete-tapahtuman (puhelut) taiweb.complete-tapahtuman (verkkopuhelut ja rakentajan mikrofonitestipuhelut) alla dokumentoidulla vakaalla hyötykuormalla, toimituskohtaisellaevent_id-tunnuksella, 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.
Pyyntö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",
"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"
}| Kenttä | Tyyppi | Kuvaus |
|---|---|---|
call_id | kokonaisluku | Pysyy samana tämän puhelun kaikissa tapahtumissa |
agent_id | kokonaisluku | null | Puhelun käsitellyt agentti, kun sellainen on määritetty |
agent_name | merkkijono | null | Puhelun käsitellyt agentti, kun sellainen on määritetty |
direction | merkkijono | inbound, outbound, web, test. Historialliset payloadit voivat sisältää vanhoja mic- tai widget-arvoja |
from_number, to_number | merkkijono | E.164. from_number on kirjaimellisesti "web" verkkopuheluissa ja testipuheluissa |
origin_domain | merkkijono | Vain web/test — sivun alkuperä, jolla widget oli isännöitynä (tyhjä mic-istunnoissa) |
start_time, end_time | aikaleima | ISO 8601 UTC |
duration_seconds | kokonaisluku | null | Johdettu aloitus- ja päättymisajasta |
status | merkkijono | completed tai failed |
end_reason | merkkijono | Katso alla oleva taulukko |
product, voice | merkkijono | Puhelun aikana käytössä ollut agentin määritys |
variables | objekti | Syötemuuttujat, joista otettiin tilannekuva puhelun alkaessa |
unresolved_variables | taulukko | Muuttujien nimet, joihin puhelun määritys viittaa mutta joita ei annettu puhelun alkaessa |
transfer_number | merkkijono | null | Asetetaan, kun puhelu siirrettiin |
recording_url | merkkijono | null | Vanheneva allekirjoitettu URL-osoite; lataa se viipymättä. null, kun tallennetta ei ole saatavilla |
recording_stopped_at | aikaleima | null | Hetki, jolloin tallennus lopetettiin ennen puhelun päättymistä; tallenne ei sisällä mitään tämän hetken jälkeen. null, kun tallennus jatkui puhelun loppuun asti tai puhelua ei tallennettu |
recording_stopped_reason | merkkijono | null | Syy siihen, miksi tallennus lopetettiin ennenaikaisesti. warm_transfer: tallennus pysähtyy juuri ennen kuin lämmin siirto soittaa kohdenumeroon, ja pysyy pois käytöstä, vaikka siirto epäonnistuisi ja soittaja palaisi agentille. Käsittele muita arvoja tiedoksi tarkoitettuina; niitä voidaan lisätä lisää. null, kun recording_stopped_at on null |
billable_minutes | numero | Laskutettavat minuutit pyöristettynä lähimpään neljännesminuuttiin (15 sekunnin välein, vähintään 0.25). Suoraan vastaajaan ohjautuvat puhelut ilmoittavat silti tässä todelliset mitatut minuuttinsa, mutta veloitus on rajattu yhteen minuuttiin sopimushinnalla. |
billing_total_cents | kokonaisluku | USD-sentit |
transcripts | taulukko | Vuorokohtaiset transkriptiomerkinnät; voi olla tyhjä, jos transkriptiota ei ole saatavilla |
summary | merkkijono | null | Selkokielinen puhelun jälkeinen yhteenveto (kahdesta neljään proosalausetta, enintään 2 000 merkkiä, kirjoitettuna puhelun kielellä: agentin ensisijaisella kielellä, ellei puhelu ohittanut sitä), kun agentilla on agentin rakennustyökalun kohdassa Kerättävät tiedot käytössä Luo puhelun yhteenveto (call_summary_enabled kohteessa Agent) ja luonti onnistui; muussa tapauksessa null, mukaan lukien puhelut, jotka ovat liian lyhyitä yhteenvetoon, puhelut ilman transkriptiota ja agentit, joiden tallennus on poistettu käytöstä. Yhteenveto luodaan samassa puhelun jälkeisessä vaiheessa kuin poiminta, joten asetus Odota poimitut tiedot ennen puhelun päättymisen webhookin lähettämistä (extraction_blocks_completion) määrittää, odottaako tämä tapahtuma sitä: odotustilassa yhteenveto on valmis ennen tämän tapahtuman lähettämistä (itse yhteenvedolla on enintään 30 sekuntia vaiheen 75 sekunnin budjetista); kun tapahtuma ei odota (never tai siirto asetuksella except_transfers), tämä kenttä on null eikä yhteenvetoa lähetetä uudelleen kohteessa call.data_extracted, joten lue se myöhemmin kohteen Call object summary-kentästä. Saatavilla myös hallintapaneelin puhelusivulla |
extracted_data | objekti | null | Jäsennelty poimintatulos, jossa ovat status, fields, evidence, verification, field_reasons ja schema_version. Jokaisella muulla kuin null-kentällä on tarkka rakenteellisesti tarkistettu lainaus (enintään 1 000 merkkiä) sekä puhujan rooli ja vuoroindeksi; mallin palauttamat pidemmät lainaukset hylätään katkaisemisen sijaan. Evidenssi on null, kun kenttä on null. verified tarkoittaa, että jokainen ehdokas sai täsmälleen yhden kelvollisen riippumattoman arvion. unavailable kattaa myös virheellisen tai osittaisen tarkistajan tulosteen; kelvollisia osittaisia arvioita sovelletaan silti, kun taas ehdokkaat ilman yhtä kelvollista arviota nollataan. status on completed, failed, exhausted, skipped tai skipped_recording_disabled; null, kun agentilla ei ollut poimintakenttiä |
steps_trace | taulukko | Vain puheluissa, joiden agentilla on vaiheita tai siirtoja. Järjestys, jossa puhelu eteni niiden läpi: jokaisella merkinnällä on t_ms (millisekunteina puhelun alkamisesta), agent (agentin avain, a ja sen tunnus), step (vaiheen tunnus tai null, kun siirto tehdään agentille, jolla ei ole vaiheita) ja via (start, model, condition, on_enter tai handoff). Sisältää vain tunnukset ja ajat, joten se säilytetään myös, kun tallennus on pois käytöstä |
collected_values | objekti | Vain puheluissa, joiden agentilla on vaiheita tai siirtoja. Arvot, jotka agentti keräsi vaiheissaan, avaimena keruukentän nimi, sekä <tool>_result jokaisen vaiheen on_enter_tool-tulokselle. Jätetään pois, kun puhelun sisältöä ei säilytetä (tallennus pois käytöstä) |
Päättymissyyt
| Arvo | Merkitys |
|---|---|
user_hangup | Etäosapuoli katkaisi puhelun ensin |
ai_hangup | Tekoäly päätti puhelun tarkoituksellisesti |
ai_transfer | Tekoäly siirsi puhelun; transfer_number on asetettu |
ai_warm_transfer | Tekoäly suoritti lämpimän (avustetun) siirron |
voicemail_hangup | Vastaaja havaittiin, ja puhelu päätettiin voicemail_action-asetuksesi mukaisesti |
max_duration | Puhelu saavutti enimmäiskestorajan: agentin tai puhelun oman rajan tai alustan 6 tunnin rajan. Puhelu, jonka tekoäly tai soittaja päättää aikarajavaroituksen jälkeen, säilyttää arvon ai_hangup tai user_hangup |
superseded | Istunto korvattiin uudemmalla |
inbound_config_webhook_failed | Saapuva puhelu webhook-tilassa olevaan numeroon: määrityswebhook ei palauttanut käyttökelpoista määritystä, joten puhelu päätettiin ennen kuin agentti vastasi. status on failed, eikä puhelua laskuteta |
unknown | Päättymissyytä ei voitu määrittää |
Transkriptin muoto
Jokainen transcripts-merkintä on yksi keskusteluvuoro. Roolit ovat
user (soittajan puhe), model (agentin puhe ja työkalukutsut),
tool (työkalutulokset) 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ökalutuloksille ja järjestelmätapahtumille |
content | merkkijono | objekti | Puheteksti tai yllä näkyvä jäsennelty objekti. Työkalukutsut: {"tool_call": name, "arguments": {…}}. Työkalutulokset: {"tool_name": name, "response": {…}} |
start_ms, end_ms | kokonaisluku | Poikkeamat puhelun alusta millisekunteina. Sisältyvät, kun äänen ajoitus tunnetaan |
ttfa_ms | kokonaisluku | Ensimmäiseen ääneen kuluva aika model-vuorolle, kun se on mitattu |
audio_url, audio_urls | merkkijono / taulukko | Vanhenevat allekirjoitetut URL-osoitteet vuoron äänelle, kun ääni on tallennettu vuorokohtaisesti |
Käytä täysin jäsenneltyä vuorohistoriaa varten (keskeytysmerkintöineen,
kuittauskehotteineen ja raakoine sijainteineen)
GET /v1/calls/{call_id}/history-päätepistettä.
Vanhan hyötykuorman erot
Vanhan yhden URL-osoitteen webhook-kirjekuori on
{"type": "telephony.complete" | "web.complete", "data": {…}}, eikä siinä ole
event_id-kenttää. Sen data eroaa päätepisteen hyötykuormasta:
Vanha valmistumisen hyötykuorma sisältää myös agent_id- ja agent_name-kentät.
- Vuorotaulukko on kentässä
history, eitranscripts-kentässä (sama vuorokaava kuin yllä). - Kenttäjoukko on puhelun päättymisen raaka raportti, ja se voi sisältää taulukon lisäksi sisäisiä kenttiä — käsittele tuntemattomia kenttiä tiedoksi tarkoitettuina.
- Verkkopuhelut (
direction: "web") jättävät pois kentätfrom_number/to_numberja lisäävät kentänorigin_domain. - Builderin mikrofonitestipuhelut raportoidaan vanhalla polulla muodossa
telephony.complete(päätepistejärjestelmä muuntaa 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) antaa 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 litterointi ja tallenteen URL-osoite asiakastietojen yhteyteen.
Lähetä litteroinnit putkeen aiheiden mallinnusta, CSAT-signaalien poimintaa tai siirtoprosentin seurantaa varten.
Avaa puhelut laadunvarmistustyökalussa ihmisen tarkastettavaksi tai aja ne oman arviointimallisi läpi.
Käynnistä ilmoitus tiimin jäsenelle siirron tai epäonnistumisen yhteydessä.