ThunderPhone 2.0 đã chính thức ra mắt.Tự thiết lập, từ 2 xu/phút.Xem thông báo ra mắt

Developer cookbook

Cấu hình động cho từng cuộc gọi

Chọn tác nhân AI trả lời — hoặc viết lại prompt và cài đặt của tác nhân — riêng cho mỗi cuộc gọi đến, dựa trên logic tùy chỉnh trong webhook do bạn kiểm soát.

Theo mặc định, mỗi số điện thoại và khóa có thể công khai đều được gán một tác nhân AI tĩnh. Khi bạn cần tùy chỉnh theo từng người gọi hoặc theo từng khách truy cập — định tuyến VIP, ngữ cảnh người dùng đã đăng nhập, kiểm thử prompt A/B — hãy chuyển sang chế độ webhook và để máy chủ của bạn quyết định.

Cách hoạt động

  1. Bạn đăng ký sự kiện telephony.incoming (điện thoại) hoặc web.incoming (widget). Cả hai đều là webhook chặn: ThunderPhone chờ tối đa 10 giây để nhận phản hồi của bạn trước khi tiếp tục cuộc gọi.
  2. ThunderPhone gửi cho bạn {call_id, from_number, to_number} (phiên widget mang các trường dành riêng cho widget thay vì số điện thoại — xem schema yêu cầu).
  3. Máy chủ của bạn phản hồi bằng cấu hình tác nhân AI (prompt, giọng nói, sản phẩm, công cụ). ThunderPhone sử dụng cấu hình đó cho cuộc gọi.
  4. Nếu bạn trả về {}, hết thời gian chờ hoặc gặp lỗi, tác nhân AI được gán tĩnh sẽ được dùng làm phương án dự phòng. Mặc định an toàn.

1. Cấu hình đích webhook

Đối với số điện thoại, hãy đăng ký điểm cuối của bạn với 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"]
  }'

Phản hồi bao gồm một secret dùng một lần — hãy lưu lại; bạn sẽ dùng nó để xác minh chữ ký.

Đối với phiên widget, hãy tạo khóa có thể công khai ở mode="webhook" với URL điểm cuối của bạn được tích hợp sẵn:

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

Widget sẽ POST đến URL này khi mỗi phiên bắt đầu.

2. Triển khai handler

Ba quy tắc thực hành:

  • Xác minh chữ ký trên mọi request (xem Xác minh chữ ký webhook). Đừng bỏ qua bước này trong môi trường dev — làm đúng một lần và tái sử dụng.
  • Phản hồi nhanh. Mười giây là giới hạn cứng, và mỗi giây đều là khoảng lặng với người gọi. Thực hiện truy vấn cơ sở dữ liệu nếu cần, nhưng đừng gọi LLM phía hạ nguồn đồng bộ — nếu bạn muốn tạo prompt động, hãy tính toán trước và lưu vào bộ nhớ đệm.
  • Dự phòng rõ ràng. Mọi trạng thái không mong đợi phải trả về {} để tác nhân AI được gán tĩnh xử lý cuộc gọi.
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. Lược đồ phản hồi

Nội dung phản hồi khớp chính xác với lược đồ phản hồi cuộc gọi đến. Các trường thường dùng:

TrườngLoạiMô tả
promptchuỗi (bắt buộc)System prompt cho tác nhân AI
voicechuỗi (bắt buộc)ID giọng nói từ GET /v1/voices
productchuỗiMặc định là spark
background_trackchuỗi | nullID âm thanh nền
acknowledgement_prompt_modechuỗiauto hoặc manual (chỉ Storm-with-ack)
acknowledgement_promptchuỗiBắt buộc khi chế độ là manual
toolsmảngLược đồ công cụ hàm nội tuyến — xem Công cụ hàm

Mẫu

Ngữ cảnh người dùng đã đăng nhập

Trong widget ở chế độ webhook, trang của khách truy cập đã biết họ là ai. Gọi webhook của bạn với tham số chuỗi truy vấn mà SDK widget chuyển tiếp (?customer_id=123) và tra cứu khách hàng ở phía máy chủ.

Triển khai prompt A/B

Trước khi tự triển khai cách này, lưu ý rằng ThunderPhone có tính năng Thử nghiệm gốc (/dashboard/experiments và tab A/B trong trình tạo tác nhân) để xác định các biến thể, phân chia lưu lượng và so sánh kết quả theo từng biến thể — không cần webhook.

Nếu vẫn cần kiểm soát ở phía webhook: băm call_id → nhóm; phân phối prompt A cho 0..49 và prompt B cho 50..99. Ghi lại nhóm bạn đã chọn trong DB riêng và sau đó đối chiếu với điểm của cuộc gọi đã hoàn tất.

Định tuyến theo thời gian

Giờ làm việc → tác nhân "hỗ trợ trực tiếp"; ngoài giờ → tác nhân "ghi lời nhắn". Chỉ cần chuyển đổi dựa trên new Date().getUTCHours() trong handler của bạn.


Bước tiếp theo