ThunderPhone 2.0 ir klāt.Sāciet uzreiz — no 2 centiem minūtē.Lasīt paziņojumu

Webhooks

telephony.complete / web.complete

Nebloķējošs tīmekļa aizķeres paziņojums, kas tiek nosūtīts, kad zvans beidzas, ar transkriptu, ieraksta URL un metriku.

Pabeigšanas notikums tiek aktivizēts pēc katra zvana beigām — ienākošās telefonijas, izejošās telefonijas, tīmekļa zvana vai testa zvana (veidotāja mikrofona sesijas). Tas ir nebloķējošs: atbildiet ar jebkuru 2xx.

Notikums tiek piegādāts abos veidos:

Pieprasījuma slodze (galapunktu piegādes)

{
  "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"
}
LauksTipsApraksts
call_idintegerNemainīgs visos šī zvana notikumos
directionstringinbound, outbound, web, test. Vēsturiskās slodzēs var būt mantotās vērtības mic vai widget
from_number, to_numberstringE.164. Tīmekļa zvaniem un testa zvaniem from_number ir burtiskā vērtība "web"
origin_domainstringTikai tīmekļa/testa zvaniem — lapas izcelsme, kurā bija iegults logrīks (mikrofona sesijām tukša)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullAtvasināts no sākuma/beigu laika
statusstringcompleted vai failed
end_reasonstringSkatiet tālāk esošo tabulu
product, voicestringZvana laikā spēkā esošā balss aģenta konfigurācija
transfer_numberstring | nullIestatīts, kad zvans tika pārsūtīts
recording_urlstring | nullParakstīts URL ar derīguma termiņu; lejupielādējiet nekavējoties. null, ja nav pieejams ieraksta artefakts
billable_minutesnumberRēķinam piemērojamās minūtes, noapaļotas līdz tuvākajai ceturtdaļminūtei (15 sekunžu intervāli, minimums 0.25). Zvani, kas uzreiz nonāk balss pastā, šeit joprojām norāda faktiskās uzskaitītās minūtes, taču maksa tiek ierobežota līdz vienai minūtei pēc plāna tarifa.
billing_total_centsintegerUSD centi
transcriptsarrayTranskripta ieraksti katrai kārtai; var būt tukšs, ja transkripts nav pieejams

Beigu iemesli

VērtībaNozīme
user_hangupAttālā puse pirmā nolika klausuli
ai_hangupMI apzināti beidza zvanu
ai_transferMI pārsūtīja zvanu; ir iestatīts transfer_number
ai_warm_transferMI pabeidza silto (ar apstiprinājumu) pārsūtīšanu
voicemail_hangupTika noteikts balss pasts, un zvans beidzās atbilstoši jūsu voicemail_action
max_durationZvans sasniedza maksimālā ilguma ierobežojumu
supersededSesiju aizstāja jaunāka sesija
unknownBeigu iemeslu nevarēja noteikt

Transkripta formāts

Katrs ieraksts transcripts ir viens sarunas gājiens. Lomas ir user (zvanītāja runa), model (balss aģenta runa un rīku izsaukumi), tool (rīku rezultāti) un system (zvana notikumi, piemēram, valodas pārslēgšana).

[
  {
    "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"] }
    }
  }
]
LauksTipsApraksts
rolevirkneuser, model, tool vai system
content_typevirknetext/plain runai; application/json rīku izsaukumiem, rīku rezultātiem un sistēmas notikumiem
contentvirkne | objektsRunas teksts vai iepriekš parādītais strukturētais objekts. Rīku izsaukumi: {"tool_call": name, "arguments": {…}}. Rīku rezultāti: {"tool_name": name, "response": {…}}
start_ms, end_msvesels skaitlisNobīdes no zvana sākuma milisekundēs. Tiek norādītas, ja ir zināms audio laiks
ttfa_msvesels skaitlisLaiks līdz pirmajam audio model gājienam, ja tas ir mērīts
audio_url, audio_urlsvirkne / masīvsDerīguma termiņa ierobežoti parakstīti URL gājiena audio ierakstam, ja audio tiek ierakstīts katram gājienam atsevišķi

Pilnībā strukturētai gājienu vēsturei (ar pārtraukumu marķieriem, apstiprinājuma uzvednēm un neapstrādātām pozīcijām) izmantojiet GET /v1/calls/{call_id}/history.

Mantotās slodzes atšķirības

Mantotās viena URL tīmekļa aizķeres aploksne ir {"type": "telephony.complete" | "web.complete", "data": {…}} bez event_id, un tās data atšķiras no galapunkta slodzes:

  • Gājienu masīvs atrodas sadaļā history, nevis transcripts (tāda pati gājienu shēma kā iepriekš).
  • Lauku kopa ir neapstrādāts zvana beigu pārskats, un tajā var būt papildu iekšējie lauki, kas nav norādīti iepriekšējā tabulā — uzskatiet nezināmos laukus par informatīviem.
  • Tīmekļa zvani (direction: "web") neietver from_number / to_number un pievieno origin_domain.
  • Builder mikrofona pārbaudes zvani mantotajā ceļā tiek ziņoti kā telephony.complete (galapunkta sistēma tos kartē uz web.complete).
  • Pārsūtīšanas koordinēšana: ja zvans beidzas ar pārsūtīšanu, mantotā tīmekļa aizķere tiek izsaukta sinhroni un var atbildēt ar {"transfer_ready": false}, lai norādītu, ka pārsūtīšanas mērķis nav gatavs. Jebkura cita atbilde (vai mantotās tīmekļa aizķeres neesamība) ļauj pārsūtīšanu turpināt. Galapunkta piegādes šim nolūkam nekad netiek izmantotas.

Piemēra apstrādātājs

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

Biežākie lietošanas gadījumi

CRM integrācija

Saglabājiet katra zvana transkriptu un ieraksta URL līdzās saviem klientu ierakstiem.

Analītika

Straumējiet transkriptus uz konveijeru tēmu modelēšanai, CSAT signālu ieguvei vai pārsūtīšanas biežuma uzraudzībai.

Kvalitātes pārbaude

Atveriet zvanus QA rīkā cilvēka pārbaudei vai apstrādājiet tos ar savu novērtēšanas modeli.

Paziņojumi

Pārsūtīšanas vai kļūmes gadījumā aktivizējiet cilvēku komandas biedru.


Saistītie resursi