ThunderPhone 2.0 on nyt julkaistu.Ota käyttöön itse – alkaen 2¢/min.Lue lisää julkistuksesta

Webhooks

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:

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äTyyppiKuvaus
call_idintegerVakaa kaikissa tämän puhelun tapahtumissa
directionstringinbound, outbound, web, test. Historialliset hyötykuormat voivat sisältää vanhat arvot mic tai widget
from_number, to_numberstringE.164. from_number on kirjaimellisesti "web" verkkopuheluissa ja testipuheluissa
origin_domainstringVain verkko/testi — sivun alkuperä, jolla widgettiä isännöitiin (tyhjä mikrofonisessioissa)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullJohdettu alku- ja loppuajasta
statusstringcompleted tai failed
end_reasonstringKatso alla oleva taulukko
product, voicestringPuhelun aikana käytössä ollut agentin määritys
transfer_numberstring | nullAsetetaan, kun puhelu siirrettiin
recording_urlstring | nullVanheneva allekirjoitettu URL; lataa nopeasti. null, kun tallenneartefaktia ei ole saatavilla
billable_minutesnumberLaskutettavat 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_centsintegerUSD-sentit
transcriptsarrayVuorokohtaiset transkriptiomerkinnät; voi olla tyhjä, kun transkriptiota ei ole saatavilla

Lopetussyyt

ArvoMerkitys
user_hangupEtäosapuoli katkaisi ensin
ai_hangupAgentti lopetti puhelun tarkoituksellisesti
ai_transferAgentti siirsi puhelun; transfer_number on asetettu
ai_warm_transferAgentti suoritti lämpimän (avustetun) siirron
voicemail_hangupVastaaja havaittiin ja puhelu päättyi voicemail_action-asetuksesi mukaisesti
max_durationPuhelu saavutti enimmäiskestorajan
supersededIstunto korvattiin uudemmalla
unknownLopetussyytä 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äTyyppiKuvaus
rolemerkkijonouser, model, tool tai system
content_typemerkkijonotext/plain puheelle; application/json työkalukutsuille, työkalun tuloksille ja järjestelmätapahtumille
contentmerkkijono | objektiPuheteksti tai yllä näytetty jäsennelty objekti. Työkalukutsut: {"tool_call": name, "arguments": {…}}. Työkalun tulokset: {"tool_name": name, "response": {…}}
start_ms, end_mskokonaislukuSiirtymät puhelun alusta millisekunteina. Sisältyvät, kun äänen ajoitus on tiedossa
ttfa_mskokonaislukumodel-vuoron ensimmäiseen ääneen kuluva aika millisekunteina, kun se on mitattu
audio_url, audio_urlsmerkkijono / taulukkoVanhenevat 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ä, ei transcripts-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ät from_number / to_number ja lisäävät kentän origin_domain.
  • Builderin mikrofonitestipuhelut raportoidaan vanhassa polussa muodossa telephony.complete (päätepistejärjestelmä yhdistää ne muotoon web.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ä

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

Yleiset käyttötapaukset

CRM-integraatio

Tallenna jokaisen puhelun transkriptio ja tallenteen URL-osoite asiakastietojesi yhteyteen.

Analytiikka

Suoratoista transkriptiot työnkulkuun aiheiden mallintamista, CSAT-signaalien poimintaa tai siirtoasteen seurantaa varten.

Laatuarviointi

Avaa puhelut QA-työkalussa ihmisen tarkistettaviksi tai suorita ne oman arviointimallisi läpi.

Ilmoitukset

Ilmoita ihmistiimin jäsenelle siirrosta / epäonnistumisesta.


Aiheeseen liittyvät