ThunderPhone 2.0 är här.Kom igång själv, från 2 cent/minut.Läs lanseringsnyheten

Webhooks

telephony.complete / web.complete

Icke-blockerande webhook som levereras när ett samtal avslutas, med transkript, inspelnings-URL och mätvärden.

En slutförandehändelse utlöses efter att varje samtal avslutas — inkommande telefoni, utgående telefoni, webbsamtal eller testsamtal (mikrofonsession i byggaren). Den är icke-blockerande: svara med valfri 2xx.

Händelsen levereras via båda vägarna:

Begäransnyttolast (slutpunktsleveranser)

{
  "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"
}
FältTypBeskrivning
call_idheltalStabilt för varje händelse för detta samtal
directionstränginbound, outbound, web, test. Historiska nyttolaster kan innehålla äldre värdena mic eller widget
from_number, to_numbersträngE.164. from_number är bokstavligen "web" för webbsamtal och testsamtal
origin_domainsträngEndast webb/test — sidans ursprung som var värd för widgeten (tomt för mikrofonsessioner)
start_time, end_timetidsstämpelISO 8601 UTC
duration_secondsheltal | nullHärleds från start/slut
statussträngcompleted eller failed
end_reasonsträngSe tabellen nedan
product, voicesträngAgentkonfigurationen som gällde vid samtalstillfället
transfer_numbersträng | nullAnges när samtalet har kopplats vidare
recording_urlsträng | nullTidsbegränsad signerad URL; ladda ned omgående. null när ingen inspelning är tillgänglig
billable_minutestalDebiterade minuter, avrundade till närmaste kvartsminut (intervall på 15 sekunder, minst 0.25). Samtal som går direkt till röstbrevlådan rapporterar fortfarande sina faktiska uppmätta minuter här, men avgiften är begränsad till en minut enligt planens pris.
billing_total_centsheltalAmerikanska cent
transcriptsmatrisTranskriptposter per tur; kan vara tom när ett transkript inte är tillgängligt

Avslutsorsaker

VärdeBetydelse
user_hangupMotparten lade på först
ai_hangupRöstagenten avslutade samtalet medvetet
ai_transferRöstagenten kopplade vidare samtalet; transfer_number anges
ai_warm_transferRöstagenten slutförde en varm (övervakad) vidarekoppling
voicemail_hangupRöstbrevlådan identifierades och samtalet avslutades enligt din voicemail_action
max_durationSamtalet nådde den maximala tidsgränsen
supersededSessionen ersattes av en nyare
unknownAvslutsorsaken kunde inte fastställas

Transkriptformat

Varje post i transcripts är en samtalstur. Roller är user (uppringarens tal), model (agentens tal och verktygsanrop), tool (verktygsresultat) och system (samtalshändelser som språkbyten).

[
  {
    "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"] }
    }
  }
]
FältTypBeskrivning
rolestränguser, model, tool eller system
content_typesträngtext/plain för tal; application/json för verktygsanrop, verktygsresultat och systemhändelser
contentsträng | objektTaltext eller det strukturerade objektet som visas ovan. Verktygsanrop: {"tool_call": name, "arguments": {…}}. Verktygsresultat: {"tool_name": name, "response": {…}}
start_ms, end_msheltalFörskjutningar från samtalsstart, i ms. Finns när ljudtiming är känd
ttfa_msheltalTid till första ljudet för en model-tur, när den mäts
audio_url, audio_urlssträng / matrisTidsbegränsade signerade URL:er för turens ljud, när det spelas in per tur

Använd GET /v1/calls/{call_id}/history för den fullständigt strukturerade turhistoriken (med avbrottsmarkörer, bekräftelsefrågor och råa positioner).

Skillnader i äldre payloads

Det äldre webhook-kuvertet med en enda URL är {"type": "telephony.complete" | "web.complete", "data": {…}} utan event_id, och dess data skiljer sig från endpoint-payloaden:

  • Turmatrisen finns under history, inte transcripts (samma tur-schema som ovan).
  • Fältuppsättningen är den råa rapporten vid samtalets slut och kan innehålla ytterligare interna fält utöver tabellen ovan — behandla okända fält som information.
  • Webbsamtal (direction: "web") utelämnar from_number / to_number och lägger till origin_domain.
  • Mikrofonsamtal för Builder rapporteras som telephony.complete på den äldre sökvägen (endpoint-systemet mappar dem till web.complete).
  • Överföringssamordning: när ett samtal avslutas med en överföring anropas den äldre webhooken synkront och kan svara {"transfer_ready": false} för att signalera att överföringens måladress inte är redo. Alla andra svar (eller ingen äldre webhook) låter överföringen fortsätta. Endpoint-leveranser konsulteras aldrig för detta.

Exempelhanterare

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

Vanliga användningsfall

CRM-integration

Spara varje samtals transkription och inspelnings-URL tillsammans med dina kundregister.

Analys

Strömma transkriptioner till en pipeline för ämnesmodellering, extrahering av CSAT-signaler eller övervakning av överföringsfrekvens.

Kvalitetsgranskning

Öppna samtal i ett QA-verktyg för mänsklig granskning, eller kör dem genom din egen utvärderingsmodell.

Aviseringar

Avisera en mänsklig teammedlem vid överföring eller fel.


Relaterat