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

Developer cookbook

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

  1. Je abonneert je op de gebeurtenis telephony.incoming (telefoon) of web.incoming (widget). Beide zijn blokkerende webhooks: ThunderPhone wacht maximaal 10 seconden op je reactie voordat de oproep verdergaat.
  2. ThunderPhone stuurt je {call_id, from_number, to_number} (widgetsessies bevatten widgetspecifieke velden in plaats van nummers — zie het aanvraagschema).
  3. Je server antwoordt met een agentconfiguratie (prompt, stem, product, tools). ThunderPhone gebruikt die configuratie voor de oproep.
  4. 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.
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, 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 ...
    pass
Express
import 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:

VeldTypeBeschrijving
prompttekenreeks (vereist)Systeemprompt voor de agent
voicetekenreeks (vereist)Spraak-ID uit GET /v1/voices
producttekenreeksStandaard ingesteld op spark
background_tracktekenreeks | nullID van omgevingsaudio
acknowledgement_prompt_modetekenreeksauto of manual (alleen Storm met bevestiging)
acknowledgement_prompttekenreeksVereist wanneer de modus manual is
toolsarrayIngesloten 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