ThunderPhone 2.0 is live.Direct zelf aan de slag, vanaf 2 cent/min.Lees de aankondiging

Webhooks

telephony.complete / web.complete

Niet-blokkerende webhook die wordt verzonden wanneer een oproep eindigt, met transcript, opname-URL en statistieken.

Een voltooiingsgebeurtenis wordt geactiveerd nadat elke oproep is beëindigd — inkomende telefonie, uitgaande telefonie, weboproep of testoproep (microfoonsessie in de builder). Deze is niet-blokkerend: reageer met een willekeurige 2xx-status.

De gebeurtenis wordt via beide paden geleverd:

Requestpayload (leveringen aan eindpunten)

{
  "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"
}
VeldTypeBeschrijving
call_idintegerStabiel voor elke gebeurtenis van deze oproep
directionstringinbound, outbound, web, test. Historische payloads kunnen verouderde waarden mic of widget bevatten
from_number, to_numberstringE.164. from_number is letterlijk "web" voor weboproepen en testoproepen
origin_domainstringAlleen web/test — de oorsprong van de pagina waarop de widget werd gehost (leeg voor microfoonsessies)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullAfgeleid van begin/einde
statusstringcompleted of failed
end_reasonstringZie de onderstaande tabel
product, voicestringAgentconfiguratie die actief was tijdens de oproep
transfer_numberstring | nullIngesteld wanneer de oproep is doorgeschakeld
recording_urlstring | nullOndertekende URL met vervaldatum; download deze tijdig. null wanneer geen opnameartefact beschikbaar is
billable_minutesnumberGefactureerde minuten, afgerond op het dichtstbijzijnde kwartier minuut (stappen van 15 seconden, minimaal 0.25). Oproepen die rechtstreeks naar voicemail gaan, rapporteren hier nog steeds hun werkelijke gemeten minuten, maar de kosten zijn gemaximeerd op één minuut tegen het pakkettarief.
billing_total_centsintegerAmerikaanse centen
transcriptsarrayTranscriptvermeldingen per beurt; kan leeg zijn wanneer een transcript niet beschikbaar is

Eindredenen

WaardeBetekenis
user_hangupExterne partij heeft als eerste opgehangen
ai_hangupAI heeft de oproep bewust beëindigd
ai_transferAI heeft de oproep doorgeschakeld; transfer_number is ingesteld
ai_warm_transferAI heeft een warme (begeleide) doorschakeling voltooid
voicemail_hangupVoicemail is gedetecteerd en de oproep is beëindigd volgens je voicemail_action
max_durationOproep heeft de maximale tijdslimiet bereikt
supersededDe sessie is vervangen door een nieuwere
unknownEindreden kon niet worden vastgesteld

Transcriptindeling

Elke invoer in transcripts is één gespreksbeurt. Rollen zijn user (spraak van de beller), model (spraak van de agent en toolaanroepen), tool (toolresultaten) en system (oproepgebeurtenissen, zoals taalwisselingen).

[
  {
    "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"] }
    }
  }
]
VeldTypeBeschrijving
rolestringuser, model, tool of system
content_typestringtext/plain voor spraak; application/json voor toolaanroepen, toolresultaten en systeemgebeurtenissen
contentstring | objectSpraaktekst, of het hierboven weergegeven gestructureerde object. Toolaanroepen: {"tool_call": name, "arguments": {…}}. Toolresultaten: {"tool_name": name, "response": {…}}
start_ms, end_msintegerOffsets vanaf het begin van de oproep, in ms. Aanwezig wanneer de audiotiming bekend is
ttfa_msintegerTijd tot eerste audio voor een model-beurt, wanneer gemeten
audio_url, audio_urlsstring / arrayVerlopende ondertekende URL's voor de audio van de beurt, wanneer deze per beurt wordt opgenomen

Gebruik voor de volledig gestructureerde beurtgeschiedenis (met onderbrekingsmarkeringen, bevestigingsprompts en onbewerkte posities) GET /v1/calls/{call_id}/history.

Verschillen in legacy-payloads

De legacy-webhookenvelop met één URL is {"type": "telephony.complete" | "web.complete", "data": {…}} zonder event_id, en de data verschilt van de endpoint-payload:

  • De beurtarray staat onder history, niet onder transcripts (hetzelfde beurtschema als hierboven).
  • De veldenset is het onbewerkte rapport aan het einde van de oproep en kan aanvullende interne velden bevatten naast de bovenstaande tabel — behandel onbekende velden als informatief.
  • Weboproepen (direction: "web") laten from_number / to_number weg en voegen origin_domain toe.
  • Mic-testoproepen in de Builder worden op het legacy-pad gerapporteerd als telephony.complete (het endpointsysteem wijst ze toe aan web.complete).
  • Overdrachtscoördinatie: wanneer een oproep eindigt met een overdracht, wordt de legacy-webhook synchroon aangeroepen en kan deze {"transfer_ready": false} retourneren om aan te geven dat het overdrachtsdoel nog niet klaar is. Elke andere reactie (of geen legacy-webhook) laat de overdracht doorgaan. Endpoint-leveringen worden hiervoor nooit geraadpleegd.

Voorbeeldhandler

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

Veelvoorkomende toepassingen

CRM-integratie

Sla het transcript en de opname-URL van elke oproep op naast je klantgegevens.

Analyse

Stream transcripties naar een pipeline voor onderwerpmodellering, CSAT-signaalextractie of monitoring van het doorverbindingspercentage.

Kwaliteitsbeoordeling

Open oproepen in een QA-tool voor menselijke beoordeling, of voer ze uit met je eigen evaluatiemodel.

Meldingen

Waarschuw een menselijk teamlid bij doorverbinding / mislukking.


Gerelateerd