ThunderPhone 2.0 kini resmi hadir.Layanan mandiri, mulai dari 2¢/menit.Baca pengumumannya

Webhooks

telephony.incoming / web.incoming

Webhook pemblokiran yang membentuk konfigurasi panggilan masuk secara real time.

Saat panggilan telepon masuk mencapai nomor tanpa agen yang ditetapkan, atau sesi widget web dimulai pada kunci yang dapat dipublikasikan dalam mode="webhook", ThunderPhone mengirimkan permintaan telephony.incoming / web.incoming yang memblokir ke URL webhook lama Anda dan menunggu hingga 10 detik untuk respons konfigurasi. Gunakan pertukaran ini untuk memilih Prompt, suara, dan alat secara dinamis untuk setiap panggilan — lihat panduan konfigurasi panggilan dinamis untuk pola lengkap dari awal hingga akhir.

Pertukaran yang memblokir tidak memiliki fallback: jika handler Anda mengembalikan status non-2xx, mengalami waktu habis, atau mengembalikan konfigurasi yang gagal validasi, panggilan ditolak (panggilan telepon tidak tersambung; permintaan sesi widget gagal dengan 502/422). Jawab dengan cepat — penelepon mendengar nada dering saat Anda menentukan pilihan.

Payload permintaan

Untuk panggilan telepon (telephony.incoming):

{
  "type": "telephony.incoming",
  "data": {
    "call_id":     987654321,
    "from_number": "+14155550199",
    "to_number":   "+15551234567"
  }
}
KolomTipeDeskripsi
call_idintegerID panggilan — tetap sama di seluruh peristiwa untuk panggilan ini
from_numberstringNomor penelepon E.164
to_numberstringTujuan E.164 (salah satu nomor ThunderPhone Anda)

Untuk sesi widget web (web.incoming), data mengidentifikasi halaman yang menyematkan widget, bukan nomor telepon:

{
  "type": "web.incoming",
  "data": {
    "call_id": 987654322,
    "origin_domain": "https://example.com",
    "publishable_key_prefix": "pk_live_a1b2"
  }
}
KolomTipeDeskripsi
call_idintegerID panggilan
origin_domainstringAsal halaman yang menghosting widget
publishable_key_prefixstringKarakter pertama dari kunci yang dapat dipublikasikan yang membuka sesi
language, primary_languagestringAda saat sesi widget meminta penggantian bahasa
voicestringAda saat sesi widget meminta penggantian suara
website_contextstringAda saat widget meneruskan konteks halaman per sesi

Skema respons

Kembalikan objek JSON yang menjelaskan konfigurasi agen untuk panggilan ini. prompt dan voice wajib diisi; semua yang lain bersifat opsional.

{
  "prompt":  "You are a helpful booking assistant for Acme Restaurant.",
  "voice":   "john",
  "product": "spark",
  "background_track": null,
  "tools":   []
}
KolomTipeWajibDeskripsi
promptstringyaSystem Prompt yang mengarahkan agen
voicestringyaID suara dari GET /v1/voices, misalnya john. voice_name diterima sebagai alias. Suara yang tidak dikenal gagal divalidasi dan panggilan ditolak
productstringtidakDefault-nya adalah spark. Yang diizinkan: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack
thinking_levelstringtidakminimal, base (default), atau extra. Ditimpa untuk produk Storm: storm-extra* memaksa extra, produk storm-* lainnya memaksa base
audio_context_modestringtidakfull (default) atau reduced
watchdog_enabledbooleantidakAktifkan pengawasan untuk panggilan ini. Default false
additional_audio_contextboolean | nulltidakSertakan beberapa giliran terakhir audio penelepon, bukan hanya giliran terbaru, untuk meningkatkan koreksi serta pengumpulan data yang banyak memuat ejaan/angka dengan sedikit tambahan latensi/biaya. Aktif secara default untuk sesi masuk dan nonaktif untuk panggilan telepon keluar; null mempertahankan default
storm_feedback_modestringtidaknone, acknowledgement (default), atau tick
languagestringtidakSingkatan untuk primary_language
primary_languagestringtidakKode bahasa, dinormalisasi (default en). Kode yang tidak dapat diuraikan menolak panggilan
has_additional_languagesbooleantidakDefault false
additional_languagesarray of stringtidakBahasa tambahan yang dapat digunakan agen
native_voice_switchingbooleantidakDefault false. Saat panggilan beralih ke bahasa lain, ganti ke suara yang merupakan penutur asli bahasa tersebut (disesuaikan berdasarkan gender), alih-alih mempertahankan suara yang dikonfigurasi
background_trackstring | nulltidakID audio ambient atau null
acknowledgement_prompt_modestringtidakauto (default) atau manual (produk Storm-with-ack)
acknowledgement_promptstringtidakDigunakan saat acknowledgement_prompt_mode="manual"
silence_interval_secondsinteger | nulltidak5–120. Detik keheningan penelepon sebelum pemeriksaan
silence_max_checkinsinteger | nulltidak1–10
silence_checkins_enabledbooleantidakDefault true
connect_tone_enabledbooleantidakDefault false
voicemail_actionstringtidakprompt (default), hangup, atau message
voicemail_messagestringtidakDigunakan saat voicemail_action="message"
agent_namestringtidakNama tampilan yang dilaporkan ke dasbor dan widget
org_namestringtidakNama tampilan organisasi untuk persona agen
toolsarraytidakSkema alat fungsi inline (lihat Function Tools)
call_idintegertidakEcho opsional dari ID panggilan pada permintaan; diabaikan

Karena prompt dan voice wajib diisi, mengembalikan {} atau respons apa pun yang gagal divalidasi akan menolak panggilan dengan 422 — tidak ada fallback agen statis pada jalur ini (nomor atau kunci dalam mode webhook tidak memiliki agen yang ditetapkan).


Batas ukuran respons


Contoh handler

Python (FastAPI)
import hashlib
import hmac
import json
import os
 
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
 
def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")
 
@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)
 
    event = json.loads(body)
    if event["type"] == "telephony.incoming":
        caller = event["data"]["from_number"]
        prompt = (
            "Greet the caller as a San Francisco local…"
            if caller.startswith("+1415")
            else "You are a friendly customer support agent…"
        )
        return {
            "prompt": prompt,
            "voice": "john",
            "product": "spark",
        }
    if event["type"] == "web.incoming":
        return {
            "prompt": "You are the website's helpful voice assistant…",
            "voice": "john",
            "product": "spark",
        }
    return {}
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
 
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
 
function verify(body, signature) {
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(body)
    .digest("hex");
  return signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
 
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
 
    if (event.type === "telephony.incoming" || event.type === "web.incoming") {
      const caller = event.data.from_number || "web";
      const prompt = caller.startsWith("+1415")
        ? "Greet the caller as a San Francisco local…"
        : "You are a friendly customer support agent…";
      return res.json({
        prompt,
        voice: "john",
        product: "spark",
      });
    }
    res.json({});
  },
);

Respons dengan function tool

Tambahkan tool agar AI dapat memanggil API Anda di tengah percakapan:

{
  "prompt":  "You are a booking assistant. Use the available tools to help customers schedule appointments.",
  "voice":   "john",
  "product": "spark",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_appointments",
        "description": "Find available appointment slots",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "service": { "type": "string" }
          },
          "required": ["date"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/search",
        "method": "POST",
        "headers": {
          "X-Api-Key": "your-key"
        }
      }
    }
  ]
}

Lembar ringkas tingkatan produk

ProdukLatensiPenalaranKonfirmasi
sparkTerendahDasar
boltRendahDitingkatkan
storm-baseSedangKuat
storm-base-with-ackSedangKuatPengisi otomatis saat berpikir
storm-extraLebih tinggiMendalam
storm-extra-with-ackLebih tinggiMendalamPengisi otomatis saat berpikir

Terkait