Overzicht van webhooks
Hoe ThunderPhone realtimegebeurtenissen levert, hoe je handtekeningen verifieert en hoe de verouderde en endpointgebaseerde leveringsmodellen zich tot elkaar verhouden.
ThunderPhone verstuurt HTTP-POST-verzoeken naar je server wanneer er iets
gebeurt tijdens een oproep — een inkomende oproep start, een oproep eindigt, een beoordelingsrun
is voltooid, een waarschuwing wordt geactiveerd, enzovoort. Er zijn twee bezorgmodellen:
Meerdere URL's, geheimen per eindpunt, gebeurtenisfilters per eindpunt
en automatische nieuwe pogingen.
Beheer via GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints.
Eén URL per organisatie. Bevat de gebeurtenissen uit de
oproeplevenscyclus, inclusief de blokkerende configuratie-uitwisselingen.
Beheerd via GET/PUT /v1/webhook.
Alle tien gebeurtenistypen in de gebeurteniscatalogus worden
geleverd via webhook-eindpunten. De zes gebeurtenissen uit de oproeplevenscyclus
(telephony.incoming, telephony.complete, telephony.tool,
web.incoming, web.complete, web.tool) worden ook verzonden naar de
verouderde webhook met één URL — als je zowel een verouderde URL als een
overeenkomend eindpunt hebt, ontvang je de gebeurtenis via beide paden.
Blokkerend gedrag (de
telephony.incoming / web.incoming-configuratie-uitwisseling
en toolverzending in webhookmodus) bestaat
uitsluitend op het verouderde pad; elke levering aan een eindpunt is een
fire-and-forgetmelding.
Payloadindeling
Leveringen aan eindpunten zijn een JSON-object met data, event_id en
type:
{
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
},
"event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
"type": "telephony.incoming"
}event_id is uniek voor elke verstuurde gebeurtenis. Het is identiek bij nieuwe pogingen
en bij elk eindpunt dat de gebeurtenis ontvangt — gebruik het voor deduplicatie.
De verouderde webhook met één URL verstuurt hetzelfde type en dezelfde data, maar
zonder event_id:
{
"type": "telephony.incoming",
"data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}Bij verzending wordt elke body canoniek geserialiseerd — sleutels alfabetisch gesorteerd, geen witruimte, UTF-8. De mooi opgemaakte voorbeelden in deze documentatie dienen alleen voor de leesbaarheid.
Bekijk de gebeurteniscatalogus voor de volledige lijst met gebeurtenis- typen en payloadvelden.
Handtekeningverificatie
Elk verzoek bevat een HMAC-SHA256-handtekening over de onbewerkte
aanvraagbody in de header X-ThunderPhone-Signature. De ondertekeningssleutel is het
secret van het eindpunt (of je webhook-secret op organisatieniveau voor
verouderde leveringen).
Stappen
- Lees de onbewerkte aanvraagbody vóór enige parsing.
- Bereken
hmac_sha256(secret, body).hexdigest(). - Vergelijk deze in constante tijd met de header
X-ThunderPhone-Signature.
We ondertekenen exact de bytes die we verzenden, en die bytes zijn de canonieke JSON-serialisatie (gesorteerde sleutels, compacte scheidingstekens). Daarom werkt verificatie aan de hand van de onbewerkte body altijd — en als je framework je alleen geparste JSON geeft, levert deze opnieuw serialiseren met gesorteerde sleutels en compacte scheidingstekens identieke bytes op. Beide methoden worden behandeld in de verificatiehandleiding.
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature or "")
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/thunderphone-webhook")
def handle():
body = request.get_data()
sig = request.headers.get("X-ThunderPhone-Signature", "")
if not verify_signature(body, sig, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
# dispatch on event["type"] …
return "", 204import crypto from "node:crypto";
import express from "express";
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (!signature || expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
const app = express();
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("X-ThunderPhone-Signature") || "";
if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// dispatch on event.type …
res.sendStatus(204);
},
);Bezorgsemantiek
Deze semantiek geldt voor leveringen aan endpoints. De verouderde webhook met één URL is één synchrone poging zonder nieuwe pogingen.
Nieuwe pogingen
Elk event wordt onmiddellijk één keer geprobeerd. Elke 2xx-respons
bevestigt de levering. Bij elke andere uitkomst (geen 2xx,
verbindingsfout, time-out) proberen we opnieuw na 1 m, 5 m, 30 m, 2 u, 6 u,
12 u en 24 u na de eerste poging — 8 pogingen verspreid over
24 uur. Als elke poging mislukt, stopt de levering en wordt het endpoint
gemarkeerd met status="failing" in
webhook-endpoints. Retourneer 2xx zodra
de payload duurzaam is geaccepteerd; verwerk deze asynchroon.
Volgorde
De leveringsvolgorde is gebaseerd op best effort. In de praktijk leveren we in de
volgorde waarin events worden verzonden, maar nieuwe pogingen kunnen bij fouten
de volgorde wijzigen. Dedupliceer en stem altijd af op call_id / object-id.
Duplicaten
Levering is minstens één keer: een nieuwe poging na een respons die we nooit
hebben gezien kan een event dupliceren. Elke nieuwe poging bevat dezelfde
event_id, dus sla verwerkte id's op en sla herhalingen over. event_id wordt
ook gedeeld tussen endpoints — twee endpoints die op hetzelfde event zijn geabonneerd,
ontvangen dezelfde event_id.
Time-outs
Leveringen aan endpoints hebben een time-out van 30 s per poging. Op het
verouderde pad verlopen blokkerende verzoeken die het gedrag van live oproepen bepalen — de
configuratie-uitwisseling voor telephony.incoming / web.incoming —
na 10 s, maar een trage respons vertraagt het opnemen van de oproep, dus probeer
binnen enkele seconden te antwoorden. Tooldispatch in webhookmodus
staat standaard 20 s toe, en tooldeclaraties kunnen een timeout op het hoogste niveau instellen.
Bron-IP's
Uitgaande webhooks zijn afkomstig uit het cloud-IP-bereik van ThunderPhone. Als je firewall een toelatingslijst vereist, neem dan contact op met support en we delen de huidige bereiken.
Kiezen tussen verouderde en endpointgebaseerde webhooks
| Functie | Verouderd (/v1/webhook) | Endpoints (/v1/developer/webhook-endpoints) |
|---|---|---|
| Aantal URL's | 1 per organisatie | Meerdere per organisatie |
| Eventdekking | Alleen telephony.* / web.* | Alle 10 eventtypen |
| Eventfilter | — | Per endpoint |
| Nieuwe pogingen | Geen | 8 pogingen in 24 u |
| Envelop | type + data | type + data + event_id |
| Secretrotatie | Vervangt één secret | Secret per endpoint |
| Uitschakelen zonder verwijderen | — | status=disabled |
| Zichtbaarheid van status | — | active / disabled / failing |
| Blokkerende configuratie-uitwisseling | Ja (telephony.incoming / web.incoming) | Nooit — alleen meldingen |
| Het beste voor | Dynamische oproepconfiguratie | Eventverwerking in productie |
Nieuwe integraties moeten events verwerken via endpointgebaseerde webhooks. Behoud (of voeg) alleen een verouderde URL toe als je oproepen dynamisch configureert op het moment van opnemen of tooldispatch in webhookmodus gebruikt — deze verzoek/respons-uitwisselingen werken alleen via het verouderde pad.
Gerelateerd
Alle eventtypen en hun payloads.
Beheer meerdere endpoints, eventfilters en secrets.
Het blokkerende verzoek dat je server moet beantwoorden om oproepen te configureren.
Payload na een oproep met transcript, opname en statistieken.