ThunderPhone 2.0 er lanceret.Selvbetjening fra 2 cent pr. minut.Læs mere om lanceringen

Webhooks

telephony.complete / web.complete

Ikke-blokerende webhook leveret, når et opkald afsluttes, med transskription, optagelses-URL og metrics.

En fuldførelseshændelse udløses, når hvert opkald afsluttes — 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 via begge stier:

Anmodningsnyttelast (endpoint-leveringer)

{
  "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"
}
FeltTypeBeskrivelse
call_idheltalStabil på tværs af alle hændelser for dette opkald
agent_idheltal | nullAgent, der håndterede opkaldet, når en var tildelt
agent_namestreng | nullAgent, der håndterede opkaldet, når en var tildelt
directionstrenginbound, outbound, web, test. Historiske payloads kan indeholde de ældre værdier mic eller widget
from_number, to_numberstrengE.164. from_number er den bogstavelige værdi "web" for webopkald og testopkald
origin_domainstrengKun web/test — sidens oprindelse, som hostede widgetten (tom for mic-sessioner)
start_time, end_timetidsstempelISO 8601 UTC
duration_secondsheltal | nullUdledt af start/slut
statusstrengcompleted eller failed
end_reasonstrengSe tabellen nedenfor
product, voicestrengAgentkonfiguration, der var aktiv på opkaldstidspunktet
variablesobjektInputvariabler som øjebliksbillede, da opkaldet startede
unresolved_variablesarrayVariabelnavne, der refereres til af opkaldskonfigurationen, men ikke blev leveret ved opkaldets start
transfer_numberstreng | nullAngives, når opkaldet blev viderestillet
recording_urlstreng | nullUdløbende signeret URL; download straks. null, når intet optagelsesartefakt er tilgængeligt
recording_stopped_attidsstempel | nullTidspunktet, hvor optagelsen blev stoppet, før opkaldet sluttede; intet efter dette tidspunkt er med i optagelsen. null, når optagelsen fortsatte til opkaldets slutning, eller opkaldet ikke blev optaget
recording_stopped_reasonstreng | nullHvorfor optagelsen stoppede tidligt. warm_transfer: den stopper lige før en varm viderestilling ringer til destinationen og forbliver slået fra, selv hvis viderestillingen mislykkes, og opkalderen vender tilbage til agenten. Behandl andre værdier som informative; der kan tilføjes flere. null, når recording_stopped_at er null
billable_minutestalFakturerede minutter, afrundet til nærmeste kvarte minut (15-sekunders intervaller, minimum 0.25). Opkald, der går direkte til telefonsvarer, rapporterer stadig deres faktiske målte minutter her, men beløbet er begrænset til ét minut til planens takst.
billing_total_centsheltalUSD-cent
transcriptsarrayTransskriptposter pr. tur; kan være tom, når et transskript ikke er tilgængeligt
summarystreng | nullLetforståelig oversigt efter opkaldet (to til fire sætninger, højst 2.000 tegn, skrevet på opkaldets sprog: agentens primære sprog, medmindre opkaldet tilsidesatte det), når agenten har Generer en opkaldsoversigt slået til under Data, der skal indsamles i agentbyggeren (call_summary_enabled på Agent), og genereringen lykkedes; ellers null, herunder for opkald, der er for korte til at opsummere, opkald uden transskript og agenter med optagelse slået fra. Oversigten genereres i samme trin efter opkaldet som udtrækningen, så indstillingen Vent på udtrukne data, før webhooken for afsluttet opkald sendes (extraction_blocks_completion) afgør, om denne hændelse venter på den: I en ventetilstand er oversigten klar, før denne hændelse sendes (selve oversigten får højst 30 sekunder inden for trinnets budget på 75 sekunder); når hændelsen ikke venter (never eller en viderestilling under except_transfers), er dette felt null, og oversigten sendes ikke igen i call.data_extracted, så læs den senere fra summary på Call-objektet. Også tilgængelig på opkaldssiden i dashboardet
extracted_dataobjekt | nullStruktureret udtrækningsresultat med status, fields, evidence, verification, field_reasons og schema_version. Hvert felt, der ikke er null, har det nøjagtige strukturelt kontrollerede citat (højst 1.000 tegn) samt talerrolle og turindeks; længere citater returneret af modellen afvises i stedet for at blive afkortet. Evidens er null, når feltet er null. verified betyder, at hver kandidat modtog præcis én gyldig uafhængig vurdering. unavailable dækker også ukorrekt formateret eller delvist output fra verificeringen; gyldige delvise vurderinger anvendes stadig, mens kandidater uden én gyldig vurdering sættes til null. status er completed, failed, exhausted, skipped eller skipped_recording_disabled; null, når agenten ikke havde nogen udtrækningsfelter
steps_tracearrayKun på opkald, hvis agent har trin eller overleveringer. Rækkefølgen, som opkaldet bevægede sig gennem dem i: Hver post har t_ms (millisekunder siden opkaldet startede), agent (agentens nøgle, a plus dens ID), step (trin-ID'et eller null ved en overlevering til en agent uden trin) og via (start, model, condition, on_enter eller handoff). Indeholder kun ID'er og tidspunkter, så den bevares også, når optagelse er slået fra
collected_valuesobjektKun på opkald, hvis agent har trin eller overleveringer. De værdier, agenten indsamlede i sine trin, med navn på indsamlingsfeltet som nøgle, samt <tool>_result for hvert trins on_enter_tool-resultat. Udelades, når opkaldets indhold ikke bevares (optagelse slået fra)

Slutårsager

VærdiBetydning
user_hangupDen eksterne part lagde på først
ai_hangupAI'en afsluttede opkaldet med vilje
ai_transferAI'en viderestillede opkaldet; transfer_number er angivet
ai_warm_transferAI'en gennemførte en varm (assisteret) viderestilling
voicemail_hangupTelefonsvarer blev registreret, og opkaldet blev afsluttet i henhold til din voicemail_action
max_durationOpkaldet nåede den maksimale varighedsgrænse: agentens eller opkaldets egen grænse eller platformens grænse på 6 timer. Et opkald, som AI'en eller den, der ringer, afslutter efter advarslen om tidsgrænsen, beholder ai_hangup eller user_hangup
supersededSessionen blev erstattet af en nyere
inbound_config_webhook_failedIndgående opkald til et nummer i webhooktilstand: konfigurationswebhooken returnerede ikke en brugbar konfiguration, så opkaldet blev afsluttet, før en agent svarede. status er failed, og opkaldet faktureres ikke
unknownAfslutningsårsagen kunne ikke fastslås

Transskriptformat

Hvert element i transcripts er én samtalevending. Roller er user (opkalderens tale), model (agentens tale og værktøjskald), tool (værktøjsresultater) og system (opkaldshændelser 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 opkaldets start, ms. Findes, når lydtiming er kendt
ttfa_msheltalTid til første lyd for en model-vending, når den er målt
audio_url, audio_urlsstreng / arrayUdløbende signerede URL'er til vendingens lyd, når den optages pr. vending

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

Forskelle i ældre payloads

Den ældre webhook-konvolut med én URL er {"type": "telephony.complete" | "web.complete", "data": {…}} uden event_id, og dens data adskiller sig fra endpointets payload:

Det ældre fuldførelsespayload indeholder også agent_id og agent_name.

  • Vendingarrayet findes under history, ikke transcripts (samme vendingsskema som ovenfor).
  • Feltsættet er den rå rapport ved opkaldets afslutning og kan indeholde yderligere interne felter ud over tabellen ovenfor — behandl ukendte felter som information.
  • Webopkald (direction: "web") udelader from_number / to_number og tilføjer origin_domain.
  • Builder-mikrofontestopkald rapporteres som telephony.complete på den ældre sti (endpointsystemet knytter dem til web.complete).
  • Overførselskoordinering: Når et opkald afsluttes i 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. Endpointleveringer konsulteres aldrig i dette tilfælde.

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 optagelses-URL sammen med dine kunderegistre.

Analyse

Stream transskriptioner til en pipeline til emnemodellering, udtrækning af CSAT-signaler eller overvågning af overførselsrater.

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 overførsel eller fejl.


Relateret