ThunderPhone 2.0 er lanceret.Selvbetjening fra 2 cent pr. minut.Læs mere om lanceringen

Developer cookbook

Dynamisk konfiguration pr. opkald

Vælg den svarende agent — eller omskriv dens prompt og indstillinger — separat for hvert indgående opkald, styret af brugerdefineret logik i en webhook, du kontrollerer.

Som standard har hvert telefonnummer og hver publicerbar nøgle en statisk agent tildelt. Når du har brug for tilpasning pr. opkalder eller pr. besøgende — VIP-routing, kontekst for indloggede brugere, A/B-tests af prompts — skal du skifte til webhooktilstand og lade din server træffe beslutningen.

Sådan fungerer det

  1. Du abonnerer på hændelsen telephony.incoming (telefon) eller web.incoming (widget). Begge er blokerende webhooks: ThunderPhone venter på dit svar, før opkaldet fortsætter, i op til 13 sekunder for et telefonopkald (inklusive ét genforsøg) og 10 sekunder for en widgetsession.
  2. ThunderPhone sender dig {call_id, from_number, to_number} (widget- sessioner indeholder widgetspecifikke felter i stedet for numre — se anmodningsskemaet).
  3. Din server svarer med en agentkonfiguration (prompt, stemme, produkt, værktøjer). ThunderPhone bruger den konfiguration til opkaldet.
  4. Der er ingen reserveagent. Hvis du returnerer {} eller en ugyldig konfiguration, eller hvis anmodningen stadig fejler efter sit genforsøg, afsluttes telefonopkaldet straks og registreres med end_reason inbound_config_webhook_failed; en widgetsessionanmodning fejler.

1. Konfigurer webhookdestinationen

For telefonnumre skal du abonnere dit endepunkt på 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"]
  }'

Svaret indeholder en secret, der kun vises én gang — gem den; du skal bruge den til signaturverifikation.

For widgetsessioner skal du oprette en publicerbar nøgle i mode="webhook" med din endepunkt-URL indbygget:

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"]
  }'

Widgetten sender en POST-anmodning til denne URL ved starten af hver session.

2. Implementer handleren

Tre tommelfingerregler:

  • Bekræft signaturen for hver anmodning (se Bekræft webhook-signaturer). Spring ikke dette over i udvikling — gør det rigtigt én gang, og genbrug det.
  • Svar hurtigt. Et telefonopkalds første forsøg afbrydes efter 10 sekunder og hele udvekslingen efter 13, og hvert sekund er stilhed for opkalderen. Foretag databaseopslag, hvis du har brug for det, men kald ikke efterfølgende LLM'er synkront — hvis du vil have dynamisk promptgenerering, skal du forudberegne og cache.
  • Fald rent tilbage. For enhver uventet tilstand skal du svare med en standard-prompt og voice (eller et gemt agent_id). Returnering af {} eller en fejlstatus afslutter opkaldet.
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. Svarskema

Svarbrødteksten matcher svarskemaet for indgående opkald præcist. De mest anvendte felter:

FeltTypeBeskrivelse
promptstreng (påkrævet)Systemprompt til agenten
voicestreng (påkrævet)Stemme-id fra GET /v1/voices
productstrengStandard er spark. Eksperimentelle produkter er kun tilgængelige for organisationer på tilladelseslisten.
background_trackstreng | nullId for baggrundslyd
acknowledgement_prompt_modestrengauto eller manual (kun Storm med bekræftelse)
acknowledgement_promptstrengPåkrævet, når tilstanden er manual
language_switchingstrengFor flersprogede agenter: ask_first eller follow_caller
toolsarrayIntegrerede skemaer for funktionsværktøjer — se Funktionsværktøjer

Behold en gemt agent, og angiv variabler

Returner {"agent_id": 12, "variables": {"name": "Ada"}} for at bruge den pågældende organisations gemte agent med data pr. opkald. Dens prompt kan indeholde {{name}} eller {{name|Friend}}. Webhook-variabler flettes med variabler på anmodningsniveau; null bruger pladsholderens standardværdi eller tom tekst, hvis ingen er angivet. De endelige værdier og uløste navne vises i opkaldsdetaljer og fuldførelses-webhooks. Svar for gemte agenter accepterer kun agent_id, variables og timezone. Hvis prompt er til stede, bruger svaret integreret konfiguration og ignorerer agent_id (herunder null eller metadata, der ikke er et heltal); den integrerede prompt skal stadig være gyldig. Svar med integreret konfiguration kan også indeholde variables. Svar for gemte agenter bruger agentens implementerede A/B-fordeling på både telefon- og widgetopkald og gengiver derefter variabler. Se opkaldsvariabler for grænser og understøttelse af sessions-API. Blokerende konfiguration kommer fra den ældre URL for telefonnummer/organisation eller en widgetnøgle i webhooktilstand; indgående hændelser fra endpointsystemet er kun notifikationer.

Angiv tidszonen pr. opkald

Én agent kan besvare opkald for kontakter i forskellige tidszoner. Tilføj timezone, et IANA-navn, til et gemt agent- eller inline-svar, så angiver agenten dato og klokkeslæt og tilbyder tidspunkter i den zone for dette opkald, forud for agentens egen tidszone:

{"agent_id": 12, "timezone": "America/Chicago", "variables": {"name": "Ada"}}

Send zonen i stedet for et tidsstempel i variables: agentens ur fortsætter med at køre under opkaldet, og et tidspunkt, du sender med, bliver forældet. En værdi, der ikke er en IANA-zone, ignoreres, og opkaldet forbindes i agentens zone.

Mønstre

Kontekst for bruger, der er logget ind

I widgets i webhooktilstand ved den besøgendes side allerede, hvem vedkommende er. Kald din webhook med en querystring-parameter, som widget-SDK'et videresender (?customer_id=123), og slå kunden op på serversiden.

A/B-udrulning af prompts

Før du selv implementerer dette, skal du være opmærksom på, at ThunderPhone har en indbygget funktion til Eksperimenter (/dashboard/experiments og fanen A/B i agentbyggeren), som definerer varianter, fordeler trafik og sammenligner resultater pr. variant — ingen webhook påkrævet.

Hvis du alligevel har brug for kontrol på webhook-siden: hash call_id → bucket; servér prompt A for 0..49 og prompt B for 50..99. Registrer den bucket, du valgte, i din egen database, og korrelér senere med det afsluttede opkalds vurdering.

Tidsbaseret routing

Åbningstid → agenten "live support"; uden for åbningstid → agenten "tag en besked". Rent skift baseret på new Date().getUTCHours() i din handler.


Næste trin