ThunderPhone 2.0 er lansert.Kom i gang selv, fra 2 ¢/min.Les mer om lanseringen

Webhooks

telephony.complete / web.complete

Ikke-blokkerende webhook som leveres når en samtale avsluttes, med transkripsjon, URL til opptak og måledata.

En fullføringshendelse utløses etter at hvert anrop avsluttes — innkommende telefoni, utgående telefoni, nettsamtale eller testanrop (mikrofonøkt i byggeren). Den er ikke-blokkerende: svar med en hvilken som helst 2xx-status.

Hendelsen leveres på begge måter:

Forespørselsnyttelast (leveringer til endepunkter)

{
  "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å tvers av alle hendelser for dette anropet
directionstringinbound, outbound, web, test. Historiske nyttelaster kan inneholde de eldre verdiene mic eller widget
from_number, to_numberstringE.164. from_number er den bokstavelige verdien "web" for nettsamtaler og testanrop
origin_domainstringKun web/test — opprinnelsen til siden som var vert for widgeten (tom for mikrofonøkter)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullUtledet fra start/slutt
statusstringcompleted eller failed
end_reasonstringSe tabellen nedenfor
product, voicestringAgentkonfigurasjonen som var aktiv på tidspunktet for anropet
transfer_numberstring | nullAngis når anropet ble overført
recording_urlstring | nullSignert URL med utløpstid; last ned raskt. null når ingen opptaksartefakt er tilgjengelig
billable_minutesnumberFakturerte minutter, avrundet til nærmeste kvartminutt (15-sekunders intervaller, minimum 0.25). Anrop som går rett til talepost, rapporterer fortsatt de faktiske målte minuttene her, men kostnaden er begrenset til ett minutt til plansatsen.
billing_total_centsintegerUSD-cent
transcriptsarrayTranskripsjonsoppføringer per samtaletur; kan være tom når en transkripsjon ikke er tilgjengelig

Avslutningsårsaker

VerdiBetydning
user_hangupDen eksterne parten la på først
ai_hangupStemmeagenten avsluttet samtalen bevisst
ai_transferStemmeagenten overførte samtalen; transfer_number er angitt
ai_warm_transferStemmeagenten fullførte en varm (assistert) overføring
voicemail_hangupTalepost ble oppdaget, og samtalen ble avsluttet i henhold til voicemail_action
max_durationAnropet nådde grensen for maksimal varighet
supersededØkten ble erstattet av en nyere økt
unknownAvslutningsårsaken kunne ikke fastslås

Transkriptformat

Hvert element i transcripts er én samtaletur. Rollene er user (innringerens tale), model (agentens tale og verktøykall), tool (verktøyresultater) og system (anropshendelser som språkbytter).

[
  {
    "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 verktøykall, verktøyresultater og systemhendelser
contentstreng | objektTaletekst eller det strukturerte objektet vist ovenfor. Verktøykall: {"tool_call": name, "arguments": {…}}. Verktøyresultater: {"tool_name": name, "response": {…}}
start_ms, end_msheltallForskyvninger fra anropsstart, i ms. Finnes når lydtidspunkt er kjent
ttfa_msheltallTid til første lyd for en model-tur, når målt
audio_url, audio_urlsstreng / matriseUtløpende signerte URL-er for turens lyd, når lyd er tatt opp per tur

Bruk den fullstendig strukturerte turhistorikken (med avbruddsmarkører, bekreftelsesforespørsler og rå posisjoner) via GET /v1/calls/{call_id}/history.

Forskjeller i eldre payload

Den eldre webhook-konvolutten med én URL er {"type": "telephony.complete" | "web.complete", "data": {…}} uten event_id, og data-feltet er forskjellig fra endepunktets payload:

  • Turmatrisen ligger under history, ikke transcripts (samme turskjema som ovenfor).
  • Feltsettet er den rå rapporten ved slutten av anropet og kan inneholde flere interne felt enn tabellen ovenfor — behandle ukjente felt som informasjon.
  • Nettanrop (direction: "web") utelater from_number / to_number og legger til origin_domain.
  • Builder-mikrofontestanrop rapporteres som telephony.complete på den eldre stien (endepunktsystemet tilordner dem til web.complete).
  • Overføringskoordinering: Når et anrop avsluttes med en overføring, blir den eldre webhooken kalt synkront og kan svare {"transfer_ready": false} for å signalisere at målet for overføringen ikke er klart. Alle andre svar (eller ingen eldre webhook) lar overføringen fortsette. Endepunktleveringer brukes aldri til dette.

Eksempel på hendelsesbehandler

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

Vanlige bruksområder

CRM-integrasjon

Lagre transkripsjonen og opptaks-URL-en for hver samtale sammen med kundepostene dine.

Analyse

Strøm transkripsjoner til en pipeline for emnemodellering, uttrekking av CSAT-signaler eller overvåking av overføringsrate.

Kvalitetssikring

Åpne samtaler i et QA-verktøy for menneskelig gjennomgang, eller kjør dem gjennom din egen evalueringsmodell.

Varsler

Varsle en menneskelig kollega ved overføring / feil.


Relatert