ThunderPhone 2.0 är här.Kom igång på egen hand, från 2 ¢/min.Läs lanseringsnyheten

Developer cookbook

Dynamisk konfiguration per samtal

Välj den svarande röstagenten – eller skriv om dess prompt och inställningar – separat för varje inkommande samtal, styrt av anpassad logik i en webhook som du kontrollerar.

Som standard har varje telefonnummer och publicerbar nyckel en statisk agent kopplad. När du behöver anpassning per uppringare eller per besökare — VIP-dirigering, kontext för inloggade användare, A/B-tester av prompter — växlar du till webhook-läge och låter din server avgöra.

Så fungerar det

  1. Du prenumererar på händelsen telephony.incoming (telefon) eller web.incoming (widget). Båda är blockerande webhookar: ThunderPhone väntar på ditt svar innan samtalet fortsätter, i upp till 13 sekunder för ett telefonsamtal (inklusive ett återförsök) och 10 sekunder för en widgetsession.
  2. ThunderPhone skickar {call_id, from_number, to_number} till dig (widget- sessioner innehåller widgetspecifika fält i stället för nummer — se begärandeschemat).
  3. Din server svarar med en agentkonfiguration (prompt, röst, produkt, verktyg). ThunderPhone använder den konfigurationen för samtalet.
  4. Det finns ingen reservagent. Om du returnerar {} eller en ogiltig konfiguration, eller om begäran fortfarande misslyckas efter sitt återförsök, avslutas telefonsamtalet direkt och registreras med end_reason inbound_config_webhook_failed; en begäran om widgetsessions misslyckas.

1. Konfigurera webhookdestinationen

För telefonnummer prenumererar du din slutpunkt 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 innehåller en secret som bara visas en gång — spara den; du använder den för signaturverifiering.

För widgetsessioner skapar du en publicerbar nyckel i mode="webhook" med din slutpunkts-URL inbäddad:

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

Widgeten skickar en POST-begäran till denna URL vid varje sessionsstart.

2. Implementera hanteraren

Tre tumregler:

  • Verifiera signaturen för varje begäran (se Verifiera webhook-signaturer). Hoppa inte över detta i utvecklingsmiljön — gör rätt en gång och återanvänd.
  • Svara snabbt. Ett telefonsamtals första försök avbryts efter 10 sekunder och hela utbytet efter 13, och varje sekund är tystnad för den som ringer. Gör databasuppslag om du behöver, men anropa inte efterföljande LLM:er synkront — om du vill generera dynamiska promptar, förberäkna och cachelagra.
  • Använd en ren reservlösning. Vid varje oväntat tillstånd ska du svara med en standard-prompt och voice (eller ett sparat agent_id). Att returnera {} eller en felstatus avslutar samtalet.
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. Svarsschema

Svarstexten matchar svarsschemat för inkommande samtal exakt. De vanligaste fälten:

FältTypBeskrivning
promptsträng (obligatoriskt)Systemprompt för agenten
voicesträng (obligatoriskt)Röst-ID från GET /v1/voices
productsträngStandardvärdet är spark. Experimentella produkter är endast tillgängliga för tillåtelselistaorganisationer.
background_tracksträng | nullID för bakgrundsljud
acknowledgement_prompt_modesträngauto eller manual (endast Storm med bekräftelse)
acknowledgement_promptsträngKrävs när läget är manual
language_switchingsträngFör flerspråkiga agenter: ask_first eller follow_caller
toolsmatrisInline-scheman för funktionsverktyg — se Funktionsverktyg

Behåll en sparad agent och ange variabler

Returnera {"agent_id": 12, "variables": {"name": "Ada"}} för att använda organisationens sparade agent med data per samtal. Dess prompt kan innehålla {{name}} eller {{name|Friend}}. Webhook-variabler slås samman ovanpå variabler på förfrågningsnivå; null använder platshållarens standardvärde eller tom text om inget anges. De slutliga värdena och olösta namnen visas i samtalsdetaljer och webhooks för slutförande. Svar för sparade agenter accepterar endast agent_id, variables och timezone. Om prompt finns används infogad konfiguration i svaret och agent_id ignoreras (inklusive null eller metadata som inte är heltal); den infogade prompten måste fortfarande vara giltig. Svar med infogad konfiguration kan också innehålla variables. Svar för sparade agenter använder agentens driftsatta A/B-fördelning för både telefon- och widget-samtal och återger sedan variablerna. Se samtalsvariabler för gränser och stöd för sessions-API. Blockerande konfiguration kommer från den äldre URL:en för telefonnummer/organisation eller en widgetnyckel i webhook-läge; inkommande händelser från slutpunktssystemet är endast aviseringar.

Ställ in tidszon per samtal

En agent kan svara kontakter i olika tidszoner. Lägg till timezone, ett IANA-namn, i ett sparat agentsvar eller ett infogat svar, så anger agenten datum och tid samt erbjuder tider i den zonen för det här samtalet, före agentens egen tidszon:

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

Skicka zonen i stället för en tidsstämpel i variables: agentens klocka fortsätter att gå under samtalet, och en tid du skickar in blir inaktuell. Ett värde som inte är en IANA-zon ignoreras och samtalet kopplas upp i agentens zon.

Mönster

Kontext för inloggad användare

I widgetar i webhookläge vet besökarens sida redan vem de är. Anropa din webhook med en frågesträngsparameter som widget-SDK:n vidarebefordrar (?customer_id=123) och slå upp kunden på serversidan.

A/B-lansering av promptar

Innan du bygger detta själv bör du notera att ThunderPhone har en inbyggd funktion för Experiment (/dashboard/experiments och fliken A/B i agentbyggaren) som definierar varianter, delar upp trafiken och jämför resultat per variant — ingen webhook krävs.

Om du ändå behöver kontroll på webhook-sidan: hasha call_id → bucket; servera prompt A för 0..49 och prompt B för 50..99. Registrera vilken bucket du valde i din egen databas och korrelera den senare med det slutförda samtalets betyg.

Tidsbaserad routing

Öppettider → agent för "live-support"; utanför öppettider → agent för "ta ett meddelande". Enbart växling på new Date().getUTCHours() i din hanterare.


Nästa steg