Open in
Dynamische configuratie per oproep
Kies voor elke inkomende oproep afzonderlijk de spraakagent die antwoordt, of herschrijf de prompt en instellingen ervan, op basis van aangepaste logica in een webhook die je zelf beheert.
Standaard heeft elk telefoonnummer en elke publiceerbare sleutel een vaste spraakagent toegewezen. Wanneer je aanpassing per beller of per bezoeker nodig hebt — VIP-routering, context van ingelogde gebruikers, A/B-tests van prompts — schakel je over naar 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 op je reactie voordat de oproep wordt voortgezet, maximaal 13 seconden voor een telefoongesprek (inclusief één nieuwe poging) en 10 seconden voor een widgetsessie. - ThunderPhone stuurt je
{call_id, from_number, to_number}(widget- sessies bevatten widgetspecifieke velden in plaats van nummers — zie het aanvraagschema). - Je server reageert met een spraakagentconfiguratie (prompt, stem, product, tools). ThunderPhone gebruikt die configuratie voor de oproep.
- Er is geen terugvalspraakagent. Als je
{}of een ongeldige configuratie retourneert, of als het verzoek na de nieuwe poging nog steeds mislukt, wordt het telefoongesprek onmiddellijk beëindigd en geregistreerd metend_reasoninbound_config_webhook_failed; een widgetsessieverzoek mislukt.
1. Configureer de webhookbestemming
Voor telefoonnummers abonneer je 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"]
}'Het antwoord bevat een eenmalige secret — sla deze op; je gebruikt deze
voor handtekeningverificatie.
Voor widgetsessies maak je een publiceerbare sleutel in mode="webhook"
waarbij de URL van je eindpunt is 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 in development — doe het één keer goed en hergebruik het.
- Reageer snel. De eerste poging van een telefoongesprek wordt na 10 seconden afgebroken en de hele uitwisseling na 13, en elke seconde is stilte voor de beller. Voer indien nodig databaseopzoekingen uit, maar roep downstream-LLM's niet synchroon aan — als je dynamische promptgeneratie wilt, bereken deze dan vooraf en sla deze in de cache op.
- Val netjes terug. Geef voor elke onverwachte status een standaardwaarde
voor
promptenvoiceterug (of een opgeslagenagent_id). Het retourneren van{}of een foutstatus beëindigt het gesprek.
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 responsbody komt exact overeen met het antwoordschema voor inkomende oproepen. De meestgebruikte velden:
| Veld | Type | Beschrijving |
|---|---|---|
prompt | tekenreeks (vereist) | Systeemprompt voor de agent |
voice | tekenreeks (vereist) | Spraak-ID van GET /v1/voices |
product | tekenreeks | Standaard ingesteld op spark. Experimentele producten zijn alleen beschikbaar voor organisaties op de toelatingslijst. |
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 | Inline schema's voor functietools — zie Functietools |
Behoud een opgeslagen agent en geef variabelen door
Retourneer {"agent_id": 12, "variables": {"name": "Ada"}} om de opgeslagen agent van die organisatie
met gegevens per oproep te gebruiken. De prompt kan {{name}} of
{{name|Friend}} bevatten. Webhookvariabelen worden samengevoegd met variabelen op verzoekniveau; null
gebruikt de standaardwaarde van de placeholder, of lege tekst als er geen is opgegeven. De uiteindelijke
waarden en niet-opgeloste namen verschijnen in oproepdetails en webhooks bij voltooiing.
Reacties van opgeslagen agents accepteren alleen agent_id, variables en timezone. Als prompt
aanwezig is, gebruikt de reactie inlineconfiguratie en wordt agent_id genegeerd (inclusief
null- of niet-gehele metagegevens); de inlineprompt moet nog steeds geldig zijn. Reacties met inline-
configuratie kunnen ook variables bevatten. Reacties van opgeslagen agents gebruiken de uitgerolde
A/B-verdeling van de agent voor zowel telefoon- als widgetoproepen en vullen vervolgens variabelen in. Zie oproepvariabelen
voor limieten en ondersteuning voor de sessie-API. Blokkerende configuratie komt van de verouderde
URL voor telefoonnummer/organisatie of een widgetsleutel in webhookmodus; binnenkomende gebeurtenissen van het
endpoint-systeem zijn uitsluitend meldingen.
Stel de tijdzone per gesprek in
Eén spraakagent kan oproepen beantwoorden voor contacten in verschillende tijdzones. Voeg timezone, een
IANA-naam, toe aan een opgeslagen agent of inline respons. De agent vermeldt dan de datum
en tijd en biedt voor dit gesprek tijden aan in die zone, vóór de eigen tijdzone van de agent:
{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}Stuur de zone in plaats van een tijdstempel in variables: de klok van de agent loopt
door tijdens het gesprek en een tijd die je doorgeeft, veroudert. Een waarde die geen
IANA-zone is, wordt genegeerd en het gesprek wordt verbonden in de zone van de agent.
Patronen
Context van ingelogde gebruiker
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, moet je weten dat ThunderPhone een native
functie voor Experiments heeft
(/dashboard/experiments en het tabblad A/B van de agentbuilder) die
varianten definieert, verkeer verdeelt en resultaten per variant vergelijkt —
geen webhook vereist.
Als je toch controle aan webhookzijde nodig hebt: hash call_id → bucket;
gebruik prompt A voor 0..49 en prompt B voor 50..99. Leg vast welke
bucket je hebt gekozen in je eigen database en koppel die later aan de
beoordeling van de voltooide oproep.
Routering op basis van tijd
Tijdens kantooruren → agent voor "live ondersteuning"; buiten kantooruren → agent
voor "een 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 hem overal.
Combineer dynamische routering met tools per agent.
Nieuwe pogingen, volgorde, time-outs.