telephony.incoming / web.incoming
خطاف ويب حاجب يضبط إعدادات مكالمة واردة في الوقت الفعلي.
عندما تصل مكالمة هاتفية واردة إلى رقم من دون وكيل
مُعيّن، أو تبدأ جلسة عنصر واجهة ويب على مفتاح قابل للنشر في
mode="webhook"، يرسل ThunderPhone طلب
telephony.incoming / web.incoming حاجبًا إلى
عنوان URL القديم للويب هوك
وينتظر ما يصل إلى 10 ثوانٍ للحصول على استجابة إعداد. استخدم هذا
التبادل لاختيار موجّه وصوت وأدوات لكل مكالمة ديناميكيًا —
راجع دليل الإعداد الديناميكي للمكالمات
للاطلاع على النمط المتكامل.
ليس للتبادل الحاجب بديل احتياطي: إذا أعاد المعالج حالة غير 2xx، أو
انتهت مهلته، أو أعاد إعدادًا يفشل التحقق من صحته، تُرفض المكالمة (لا
تتصل المكالمة الهاتفية؛ ويفشل طلب جلسة عنصر الواجهة بالرمز 502/422).
استجب بسرعة — يسمع المتصل نغمة الرنين أثناء اتخاذك القرار.
حمولة الطلب
للمكالمات الهاتفية (telephony.incoming):
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| الحقل | النوع | الوصف |
|---|---|---|
call_id | عدد صحيح | معرّف المكالمة — ثابت عبر جميع أحداث هذه المكالمة |
from_number | سلسلة نصية | رقم المتصل بتنسيق E.164 |
to_number | سلسلة نصية | وجهة E.164 (أحد أرقام ThunderPhone الخاصة بك) |
بالنسبة إلى جلسات عنصر واجهة الويب (web.incoming)، تحدد data
الصفحة المضمّنة بدلًا من أرقام الهاتف:
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| الحقل | النوع | الوصف |
|---|---|---|
call_id | عدد صحيح | معرّف المكالمة |
origin_domain | سلسلة نصية | مصدر الصفحة التي تستضيف عنصر الواجهة |
publishable_key_prefix | سلسلة نصية | الأحرف الأولى من المفتاح القابل للنشر الذي فتح الجلسة |
language, primary_language | سلسلة نصية | يظهران عندما تطلب جلسة عنصر الواجهة تجاوزًا للغة |
voice | سلسلة نصية | يظهر عندما تطلب جلسة عنصر الواجهة تجاوزًا للصوت |
website_context | سلسلة نصية | يظهر عندما يمرر عنصر الواجهة سياق الصفحة لكل جلسة |
مخطط الاستجابة
أعِد كائن JSON يصف إعدادات الوكيل لهذه المكالمة.
الحقلان prompt وvoice مطلوبان؛ وكل ما عداهما اختياري.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
prompt | سلسلة نصية | نعم | موجّه النظام الذي يدير الوكيل |
voice | سلسلة نصية | نعم | معرّف الصوت من GET /v1/voices، مثل john. يُقبل voice_name كاسم مستعار. تفشل الأصوات غير المعروفة في التحقق وتُرفض المكالمة |
product | سلسلة نصية | لا | القيمة الافتراضية هي spark. القيم المسموح بها: spark وbolt وstorm-base وstorm-base-with-ack وstorm-extra وstorm-extra-with-ack |
thinking_level | سلسلة نصية | لا | minimal أو base (الافتراضي) أو extra. يُتجاوز لمنتجات Storm: يفرض storm-extra* القيمة extra، وتفرض منتجات storm-* الأخرى القيمة base |
audio_context_mode | سلسلة نصية | لا | full (الافتراضي) أو reduced |
watchdog_enabled | منطقي | لا | فعّل الإشراف لهذه المكالمة. القيمة الافتراضية false |
additional_audio_context | منطقي | فارغ | لا | تضمين آخر بضعة أدوار من صوت المتصل بدلًا من الدور الأحدث فقط، مما يحسّن التصحيحات وجمع البيانات التي تكثر فيها التهجئة والأرقام، مع زيادة طفيفة في زمن الاستجابة والتكلفة. يكون مفعّلًا افتراضيًا للجلسات الواردة ومعطّلًا للمكالمات الهاتفية الصادرة؛ وتحافظ القيمة null على الإعداد الافتراضي |
storm_feedback_mode | سلسلة نصية | لا | none أو acknowledgement (الافتراضي) أو tick |
language | سلسلة نصية | لا | اختصار لـ primary_language |
primary_language | سلسلة نصية | لا | رمز اللغة، مُطبّعًا (الافتراضي en). ترفض الرموز غير القابلة للحل المكالمة |
has_additional_languages | منطقي | لا | القيمة الافتراضية false |
additional_languages | مصفوفة من السلاسل النصية | لا | لغات إضافية قد ينتقل الوكيل إليها |
native_voice_switching | منطقي | لا | القيمة الافتراضية false. عند انتقال المكالمة إلى لغة أخرى، بدّل إلى صوت أصلي لتلك اللغة (مطابق للجنس) بدلًا من الاحتفاظ بالصوت المُعدّ |
background_track | سلسلة نصية | فارغ | لا | معرّف الصوت المحيط أو null |
acknowledgement_prompt_mode | سلسلة نصية | لا | auto (الافتراضي) أو manual (لمنتجات Storm المزودة بالإقرار) |
acknowledgement_prompt | سلسلة نصية | لا | يُستخدم عندما يكون acknowledgement_prompt_mode="manual" |
silence_interval_seconds | عدد صحيح | فارغ | لا | 5–120. ثواني صمت المتصل قبل إجراء تحقق |
silence_max_checkins | عدد صحيح | فارغ | لا | 1–10 |
silence_checkins_enabled | منطقي | لا | القيمة الافتراضية true |
connect_tone_enabled | منطقي | لا | القيمة الافتراضية false |
voicemail_action | سلسلة نصية | لا | prompt (الافتراضي) أو hangup أو message |
voicemail_message | سلسلة نصية | لا | يُستخدم عندما يكون voicemail_action="message" |
agent_name | سلسلة نصية | لا | اسم العرض الذي يُبلّغ به إلى لوحات المعلومات والأداة |
org_name | سلسلة نصية | لا | اسم عرض المؤسسة لشخصية الوكيل |
tools | مصفوفة | لا | مخططات أدوات الدوال المضمنة (راجع أدوات الدوال) |
call_id | عدد صحيح | لا | تكرار اختياري لمعرّف المكالمة في الطلب؛ يُتجاهل |
بما أن prompt وvoice مطلوبان، فإن إعادة {} أو أي
استجابة تفشل في التحقق ترفض المكالمة بالرمز 422 — لا توجد
آلية احتياطية لوكيل ثابت في هذا المسار (فلا يكون للرقم أو المفتاح
في وضع خطاف الويب وكيل معيّن).
حد حجم الاستجابة
مثال على معالج
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 {}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({});
},
);استجابة تتضمن أدوات دوال
أرفق أدوات كي يتمكن الذكاء الاصطناعي من استدعاء واجهات برمجة التطبيقات الخاصة بك أثناء المحادثة:
{
"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"
}
}
}
]
}دليل سريع لمستويات المنتجات
| المنتج | زمن الاستجابة | الاستدلال | الإقرار |
|---|---|---|---|
spark | الأدنى | أساسي | — |
bolt | منخفض | محسّن | — |
storm-base | متوسط | قوي | — |
storm-base-with-ack | متوسط | قوي | نص تلقائي أثناء التفكير |
storm-extra | أعلى | عميق | — |
storm-extra-with-ack | أعلى | عميق | نص تلقائي أثناء التفكير |