ThunderPhone 2.0 er lanceret.Selvbetjening fra 2 cent/min.Læs mere om lanceringen

Webhooks

telephony.complete / web.complete

Ikke-blokerende webhook leveret, når et opkald afsluttes, med transskription, URL til optagelse og målinger.

En fuldførelseshændelse udløses, efter hvert opkald slutter — indgående telefoni, udgående telefoni, webopkald eller testopkald (mikrofonsession i builderen). Den er ikke-blokerende: svar med en vilkårlig 2xx-statuskode.

Hændelsen leveres ad begge veje:

Request-payload (leveringer til slutpunkter)

{
  "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"
}
FeltTypeBeskrivelse
call_idintegerStabil på tværs af alle hændelser for dette opkald
directionstringinbound, outbound, web, test. Historiske payloads kan indeholde de ældre værdier mic eller widget
from_number, to_numberstringE.164. from_number er bogstaveligt "web" for webopkald og testopkald
origin_domainstringKun web/test — sideoprindelsen, der hostede widgetten (tom for mikrofonsessioner)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullUdledt fra start/slut
statusstringcompleted eller failed
end_reasonstringSe tabellen nedenfor
product, voicestringAgentkonfiguration, der var aktiv på opkaldstidspunktet
transfer_numberstring | nullAngives, når opkaldet blev viderestillet
recording_urlstring | nullUdløbende signeret URL; download straks. null, når intet optagelsesartefakt er tilgængeligt
billable_minutesnumberFakturerede minutter, afrundet til nærmeste kvarte minut (intervaller på 15 sekunder, minimum 0.25). Opkald, der går direkte til telefonsvarer, rapporterer stadig deres faktiske målte minutter her, men opkrævningen er begrænset til ét minut til abonnementsprisen.
billing_total_centsintegerAmerikanske cents
transcriptsarrayTransskriptposter pr. tur; kan være tomt, når et transskript ikke er tilgængeligt

Årsager til afslutning

VærdiBetydning
user_hangupModparten lagde på først
ai_hangupAgenten afsluttede opkaldet bevidst
ai_transferAgenten viderestillede opkaldet; transfer_number er angivet
ai_warm_transferAgenten fuldførte en varm (assisteret) viderestilling
voicemail_hangupTelefonsvarer blev registreret, og opkaldet blev afsluttet i henhold til din voicemail_action
max_durationOpkaldet nåede grænsen for maksimal varighed
supersededSessionen blev erstattet af en nyere
unknownÅrsagen til afslutningen kunne ikke fastslås

Transkriptformat

Hvert element i transcripts er én samtaletur. Roller er user (opkalderens tale), model (agentens tale og værktøjskald), tool (værktøjsresultater) og system (opkaldshændelser såsom sprogskift).

[
  {
    "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"] }
    }
  }
]
FeltTypeBeskrivelse
rolestrenguser, model, tool eller system
content_typestrengtext/plain for tale; application/json for værktøjskald, værktøjsresultater og systemhændelser
contentstreng | objektTaletekst eller det strukturerede objekt vist ovenfor. Værktøjskald: {"tool_call": name, "arguments": {…}}. Værktøjsresultater: {"tool_name": name, "response": {…}}
start_ms, end_msheltalForskydninger fra opkaldsstart, ms. Findes, når lydtiming er kendt
ttfa_msheltalTid til første lyd for en model-tur, når den er målt
audio_url, audio_urlsstreng / arrayUdløbende signerede URL'er til turens lyd, når lyd optages pr. tur

Brug den fuldt strukturerede turhistorik (med afbrydelsesmarkører, bekræftelsesprompter og rå positioner) med GET /v1/calls/{call_id}/history.

Forskelle i ældre payloads

Det ældre webhook-omslag med enkelt-URL er {"type": "telephony.complete" | "web.complete", "data": {…}} uden event_id, og dets data adskiller sig fra endpoint-payloadet:

  • Turarrayet ligger under history, ikke transcripts (samme turskema som ovenfor).
  • Feltsættet er den rå rapport ved opkaldets afslutning og kan omfatte yderligere interne felter ud over tabellen ovenfor — behandl ukendte felter som informative.
  • Webopkald (direction: "web") udelader from_number / to_number og tilføjer origin_domain.
  • Builder-mikrofontestopkald rapporteres som telephony.complete på den ældre sti (endpoint-systemet knytter dem til web.complete).
  • Overførselskoordinering: Når et opkald slutter med en overførsel, kaldes den ældre webhook synkront og kan svare {"transfer_ready": false} for at signalere, at overførselsmålet ikke er klar. Ethvert andet svar (eller ingen ældre webhook) lader overførslen fortsætte. Endpoint-leveringer konsulteres aldrig til dette.

Eksempel på handler

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

Almindelige anvendelsesområder

CRM-integration

Gem hvert opkalds transskription og URL til optagelse sammen med dine kundeposter.

Analyse

Stream transskriptioner til en pipeline til emnemodellering, udtræk af CSAT-signaler eller overvågning af viderestillingsrate.

Kvalitetsgennemgang

Åbn opkald i et QA-værktøj til menneskelig gennemgang, eller kør dem gennem din egen evalueringsmodel.

Notifikationer

Underret en menneskelig kollega ved viderestilling eller fejl.


Relateret