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

Webhooks

telephony.complete / web.complete

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

Een voltooiingsevent wordt geactiveerd nadat elk gesprek is beëindigd — inkomende telefonie, uitgaande telefonie, webgesprek of testgesprek (microfoonsessie in de builder). Het is niet-blokkerend: reageer met een willekeurige 2xx.

Het event wordt via beide paden geleverd:

Aanvraagpayload (endpointleveringen)

{
  "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"
}
VeldTypeBeschrijving
call_idintegerStabiel in elke gebeurtenis voor deze oproep
agent_idinteger | nullAgent die de oproep heeft afgehandeld, wanneer er een was toegewezen
agent_namestring | nullAgent die de oproep heeft afgehandeld, wanneer er een was toegewezen
directionstringinbound, outbound, web, test. Historische payloads kunnen verouderde waarden mic of widget bevatten
from_number, to_numberstringE.164. from_number is de letterlijke waarde "web" voor weboproepen en testoproepen
origin_domainstringAlleen web/test — de pagina-origin waarop de widget werd gehost (leeg voor mic-sessies)
start_time, end_timetimestampISO 8601 UTC
duration_secondsinteger | nullAfgeleid van start/einde
statusstringcompleted of failed
end_reasonstringZie de onderstaande tabel
product, voicestringAgentconfiguratie die van kracht was op het moment van de oproep
variablesobjectInvoervariabelen waarvan een momentopname is gemaakt toen de oproep begon
unresolved_variablesarrayVariabelenamen waarnaar de oproepconfiguratie verwijst maar die niet bij het begin van de oproep zijn opgegeven
transfer_numberstring | nullIngesteld wanneer de oproep is doorgeschakeld
recording_urlstring | nullVerlopende ondertekende URL; download deze snel. null wanneer geen opname-artefact beschikbaar is
recording_stopped_attimestamp | nullWanneer de opname werd gestopt voordat de oproep eindigde; niets na dit moment staat in de opname. null wanneer de opname tot het einde van de oproep liep of de oproep niet werd opgenomen
recording_stopped_reasonstring | nullWaarom de opname vroegtijdig stopte. warm_transfer: deze stopt vlak voordat een warme doorverbinding de bestemming belt, en blijft uitgeschakeld, zelfs als de doorverbinding mislukt en de beller terugkeert naar de agent. Behandel andere waarden als informatief; er kunnen meer waarden worden toegevoegd. null wanneer recording_stopped_at null 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 paktettarief.
billing_total_centsintegerUSD-centen
transcriptsarrayTranscriptitems per beurt; kan leeg zijn wanneer een transcript niet beschikbaar is
summarystring | nullSamenvatting na de oproep in gewone taal (twee tot vier zinnen proza, maximaal 2.000 tekens, geschreven in de taal van de oproep: de primaire taal van de agent, tenzij de oproep deze overschreef) wanneer de agent Een oproepsamenvatting genereren heeft ingeschakeld onder Te verzamelen gegevens in de agentbouwer (call_summary_enabled op de Agent) en het genereren is geslaagd; anders null, ook voor oproepen die te kort zijn om samen te vatten, oproepen zonder transcript en agents waarvoor opname is uitgeschakeld. De samenvatting wordt gegenereerd in dezelfde stap na de oproep als extractie, dus de instelling Wachten op geëxtraheerde gegevens voordat de webhook voor een voltooide oproep wordt verzonden (extraction_blocks_completion) bepaalt of deze gebeurtenis erop wacht: in een wachtmodus is de samenvatting gereed voordat deze gebeurtenis wordt verzonden (de samenvatting zelf krijgt maximaal 30 seconden binnen het budget van 75 seconden van de stap); wanneer de gebeurtenis niet wacht (never, of een doorverbinding onder except_transfers) is dit veld null en wordt de samenvatting niet opnieuw verzonden in call.data_extracted, dus lees deze later uit summary op het Oproepobject. Ook beschikbaar op de oproeppagina in het dashboard
extracted_dataobject | nullGestructureerd extractieresultaat met status, fields, evidence, verification, field_reasons en schema_version. Elk niet-nullveld bevat het exacte structureel gecontroleerde citaat (maximaal 1.000 tekens), plus de sprekersrol en beurtindex; langere door het model geretourneerde citaten worden afgewezen in plaats van afgekapt. Bewijs is null wanneer het veld null is. verified betekent dat elke kandidaat precies één geldig onafhankelijk oordeel heeft ontvangen. unavailable omvat ook onjuist gevormde of gedeeltelijke uitvoer van de verificateur; geldige gedeeltelijke oordelen blijven van toepassing, terwijl kandidaten zonder één geldig oordeel op null worden gezet. status is completed, failed, exhausted, skipped of skipped_recording_disabled; null wanneer de agent geen extractievelden had
steps_tracearrayAlleen voor oproepen waarvan de agent stappen of overdrachten heeft. De volgorde waarin de oproep deze doorliep: elk item bevat t_ms (milliseconden sinds de oproep begon), agent (de sleutel van de agent, a plus de ID), step (de stap-ID, of null bij een overdracht naar een agent zonder stappen) en via (start, model, condition, on_enter of handoff). Bevat alleen ID's en tijden, dus blijft bewaard, ook wanneer opname is uitgeschakeld
collected_valuesobjectAlleen voor oproepen waarvan de agent stappen of overdrachten heeft. De waarden die de agent in zijn stappen heeft verzameld, ingedeeld op naam van het verzamelveld, plus <tool>_result voor het on_enter_tool-resultaat van elke stap. Weggelaten wanneer de inhoud van de oproep niet wordt bewaard (opname uitgeschakeld)

Eindredenen

WaardeBetekenis
user_hangupExterne partij heeft als eerste opgehangen
ai_hangupAI heeft het gesprek bewust beëindigd
ai_transferAI heeft het gesprek doorverbonden; transfer_number is ingesteld
ai_warm_transferAI heeft een warme (begeleide) doorverbinding voltooid
voicemail_hangupVoicemail is gedetecteerd en het gesprek is beëindigd volgens je voicemail_action
max_durationHet gesprek heeft de maximale duur bereikt: de limiet van de agent, de limiet van het gesprek zelf of de platformlimiet van 6 uur. Een gesprek dat de AI of de beller na de waarschuwing voor de tijdslimiet beëindigt, behoudt ai_hangup of user_hangup
supersededDe sessie is vervangen door een nieuwere
inbound_config_webhook_failedInkomend gesprek naar een nummer in webhookmodus: de configuratiewebhook heeft geen bruikbare configuratie geretourneerd, waardoor het gesprek is beëindigd voordat een agent opnam. status is failed en het gesprek wordt niet gefactureerd
unknownDe reden voor beëindiging 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_msintegerVerschuivingen vanaf het begin van de oproep, in ms. Aanwezig wanneer audiotiming bekend is
ttfa_msintegerTijd tot de eerste audio voor een model-beurt, indien gemeten
audio_url, audio_urlsstring / arrayVerlopende ondertekende URL's voor de audio van de beurt, indien per beurt opgenomen

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

Verschillen met de legacy-payload

De legacy webhook-envelope met één URL is {"type": "telephony.complete" | "web.complete", "data": {…}} met geen event_id, en de data wijkt af van de endpoint-payload:

De legacy voltooiingspayload bevat ook agent_id en agent_name.

  • De beurtarray staat onder history, niet onder transcripts (hetzelfde beurtenschema als hierboven).
  • De veldenverzameling is het onbewerkte eindrapport 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 gereed is. Elke andere respons (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 pijplijn voor onderwerpenmodellering, extractie van CSAT-signalen of monitoring van overdrachtspercentages.

Kwaliteitscontrole

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

Meldingen

Activeer een menselijk teamlid bij overdracht / storing.


Gerelateerd