Dynamische configuratie per oproep
Kies de antwoordende spraakagent — of herschrijf de prompt en instellingen — afzonderlijk voor elke inkomende oproep, aangestuurd door aangepaste logica in een webhook die je beheert.
Standaard heeft elk telefoonnummer en elke publiceerbare sleutel een statische spraakagent toegewezen. Wanneer je per beller of per bezoeker wilt aanpassen — VIP-routering, context van ingelogde gebruikers, A/B-tests voor prompts — schakel je over naar de webhookmodus en laat je server beslissen.
Zo werkt het
- Je abonneert je op de gebeurtenis
telephony.incoming(telefoon) ofweb.incoming(widget). Beide zijn blokkerende webhooks: ThunderPhone wacht maximaal 10 seconden op je reactie voordat de oproep verdergaat. - ThunderPhone stuurt je
{call_id, from_number, to_number}(widgetsessies bevatten widgetspecifieke velden in plaats van nummers — zie het aanvraagschema). - Je server antwoordt met een agentconfiguratie (prompt, stem, product, tools). ThunderPhone gebruikt die configuratie voor de oproep.
- Als je
{}retourneert, een time-out optreedt of er een fout ontstaat, wordt de statisch toegewezen spraakagent als terugvaloptie gebruikt. Veilige standaardinstelling.
1. Configureer de webhookbestemming
Abonneer voor telefoonnummers je eindpunt op telephony.incoming:
curl -X POST https://api.thunderphone.com/v1/developer/webhook-endpoints \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Prod call-incoming",
"url": "https://example.com/thunderphone/incoming",
"events": ["telephony.incoming"]
}'De reactie bevat een eenmalige secret — sla deze op; je gebruikt deze
voor handtekeningverificatie.
Maak voor widgetsessies een publiceerbare sleutel in mode="webhook"
met de URL van je eindpunt ingebouwd:
curl -X POST https://api.thunderphone.com/v1/publishable-key \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Dynamic widget",
"mode": "webhook",
"webhook_url": "https://example.com/thunderphone/widget-incoming",
"allowed_domains": ["example.com"]
}'De widget voert bij elke sessiestart een POST uit naar deze URL.
2. Implementeer de handler
Drie vuistregels:
- Verifieer de handtekening bij elk verzoek (zie Webhookhandtekeningen verifiëren). Sla dit niet over tijdens de ontwikkeling — zorg dat het één keer goed staat en hergebruik het.
- Reageer snel. Tien seconden is de harde limiet, en elke seconde is stilte voor de beller. Voer databaseopzoekingen uit als dat nodig is, maar roep downstream-LLM's niet synchroon aan — als je dynamische promptgeneratie wilt, bereken deze dan vooraf en cache hem.
- Val netjes terug. Elke onverwachte status moet
{}retourneren, zodat de statisch toegewezen agent de oproep afhandelt.
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, sig: str) -> bool:
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig or "")
@app.post("/thunderphone/incoming")
async def incoming(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(401)
event = json.loads(body)
if event["type"] not in ("telephony.incoming", "web.incoming"):
return {} # fall back to default
caller = event["data"]["from_number"]
# Cheap DB lookup: is this a known VIP?
customer = lookup_customer(caller)
if customer and customer.tier == "vip":
return {
"prompt": f"You are a VIP concierge for {customer.name}. Be proactive…",
"voice": "john",
"product": "storm-base",
}
return {} # default agent handles non-VIPs
def lookup_customer(phone: str):
# ... your CRM integration ...
passimport crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, sig) {
const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
return sig &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
app.post(
"/thunderphone/incoming",
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"));
const IMPORTANT_TYPES = new Set([
"telephony.incoming",
"web.incoming",
]);
if (!IMPORTANT_TYPES.has(event.type)) return res.json({});
const customer = await lookupCustomer(event.data.from_number);
if (customer?.tier === "vip") {
return res.json({
prompt: `You are a VIP concierge for ${customer.name}. Be proactive…`,
voice: "john",
product: "storm-base",
});
}
res.json({}); // fall back to default agent
},
);3. Antwoordschema
De hoofdtekst van het antwoord komt exact overeen met het antwoordschema voor inkomende oproepen. De veelgebruikte velden:
| Veld | Type | Beschrijving |
|---|---|---|
prompt | tekenreeks (vereist) | Systeemprompt voor de agent |
voice | tekenreeks (vereist) | Spraak-ID uit GET /v1/voices |
product | tekenreeks | Standaard ingesteld op spark |
background_track | tekenreeks | null | ID van omgevingsaudio |
acknowledgement_prompt_mode | tekenreeks | auto of manual (alleen Storm met bevestiging) |
acknowledgement_prompt | tekenreeks | Vereist wanneer de modus manual is |
tools | array | Ingesloten schema's voor functietools — zie Function Tools |
Patronen
Context van ingelogde gebruikers
In widgets in webhookmodus weet de pagina van de bezoeker al wie deze
is. Roep je webhook aan met een querystringparameter die de widget-SDK
doorstuurt (?customer_id=123) en zoek de klant server-side op.
A/B-uitrol van prompts
Voordat je dit zelf implementeert, let op dat ThunderPhone een ingebouwde
functie voor Experimenten heeft
(/dashboard/experiments en het tabblad A/B van de agentbuilder) die
varianten definieert, verkeer splitst en resultaten per variant vergelijkt —
geen webhook vereist.
Als je toch controle aan de webhookzijde nodig hebt: hash call_id → bucket;
serveer prompt A voor 0..49 en prompt B voor 50..99. Sla de gekozen
bucket op in je eigen database en correleer deze later met de beoordeling van
de voltooide oproep.
Routering op basis van tijd
Tijdens kantooruren → spraakagent voor "live ondersteuning"; buiten kantooruren → spraakagent voor "bericht opnemen".
Een eenvoudige switch op new Date().getUTCHours() in je handler.
Volgende stappen
Exacte aanvraag- en antwoordschema's, inclusief elke configuratiesleutel.
Stel de HMAC één keer correct in; hergebruik deze overal.
Combineer dynamische routering met tools per spraakagent.
Nieuwe pogingen, volgorde, time-outs.