ThunderPhone 2.0 متاح الآن.خدمة ذاتية، ابتداءً من 2 سنت/دقيقة.اقرأ الإعلان

Webhooks

telephony.complete / web.complete

خطاف ويب غير حاجب يُرسَل عند انتهاء مكالمة، ويتضمن النص المفرغ ورابط التسجيل والمقاييس.

يُطلَق حدث الإكمال بعد انتهاء كل مكالمة — الاتصالات الهاتفية الواردة، والاتصالات الهاتفية الصادرة، ومكالمات الويب، أو المكالمات التجريبية (جلسة ميكروفون أداة الإنشاء). وهو غير حاجب: استجب بأي رمز 2xx.

يُسلَّم الحدث عبر كلا المسارين:

حمولة الطلب (عمليات التسليم إلى نقاط النهاية)

{
  "data": {
    "billable_minutes": 1.25,
    "billing_total_cents": 8,
    "call_id": 987654321,
    "direction": "inbound",
    "duration_seconds": 54,
    "end_reason": "user_hangup",
    "end_time": "2026-04-20T18:25:04.822Z",
    "from_number": "+14155550199",
    "product": "spark",
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "to_number": "+15551234567",
    "transcripts": [ /* see Transcript format */ ],
    "transfer_number": null,
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
الحقلالنوعالوصف
call_idعدد صحيحثابت عبر كل حدث لهذه المكالمة
directionسلسلة نصيةinbound، وoutbound، وweb، وtest. قد تحتوي الحمولات السابقة على القيم القديمة mic أو widget
from_number، to_numberسلسلة نصيةE.164. تكون قيمة from_number الحرفية هي "web" لمكالمات الويب والمكالمات التجريبية
origin_domainسلسلة نصيةللويب/الاختبار فقط — مصدر الصفحة التي استضافت الأداة (فارغ لجلسات الميكروفون)
start_time، end_timeطابع زمنيISO 8601 UTC
duration_secondsعدد صحيح | nullمشتق من وقت البدء/الانتهاء
statusسلسلة نصيةcompleted أو failed
end_reasonسلسلة نصيةراجع الجدول أدناه
product، voiceسلسلة نصيةإعدادات الوكيل السارية وقت المكالمة
transfer_numberسلسلة نصية | nullتُعيَّن عند تحويل المكالمة
recording_urlسلسلة نصية | nullرابط موقّع تنتهي صلاحيته؛ نزّله فورًا. تكون القيمة null عند عدم توفر ملف تسجيل
billable_minutesرقمالدقائق المفوترة، مقربة إلى أقرب ربع دقيقة (بزيادات قدرها 15 ثانية، وبحد أدنى 0.25). لا تزال مكالمات التحويل المباشر إلى البريد الصوتي تعرض دقائقها الفعلية المقاسة هنا، لكن تُحدَّد الرسوم بحد أقصى دقيقة واحدة وفق سعر الباقة.
billing_total_centsعدد صحيحسنتات الدولار الأمريكي
transcriptsمصفوفةإدخالات النص المفرغ لكل دور؛ قد تكون فارغة عند عدم توفر نص مفرغ

أسباب الإنهاء

القيمةالمعنى
user_hangupأنهى الطرف الآخر المكالمة أولًا
ai_hangupأنهى الذكاء الاصطناعي المكالمة عمدًا
ai_transferحوّل الذكاء الاصطناعي المكالمة؛ وتُعيَّن قيمة transfer_number
ai_warm_transferأكمل الذكاء الاصطناعي تحويلًا دافئًا (بحضور الطرفين)
voicemail_hangupتم اكتشاف البريد الصوتي وانتهت المكالمة وفق voicemail_action الخاص بك
max_durationبلغت المكالمة الحد الأقصى للمدة
supersededاستُبدلت الجلسة بجلسة أحدث
unknownتعذر تحديد سبب الإنهاء

تنسيق النص المفرغ

كل إدخال في transcripts هو دور واحد في المحادثة. الأدوار هي user (كلام المتصل)، وmodel (كلام الوكيل واستدعاءات الأدوات)، وtool (نتائج الأدوات)، وsystem (أحداث المكالمة مثل تبديل اللغة).

[
  {
    "role": "user",
    "content_type": "text/plain",
    "content": "Hi, I'm calling about my appointment.",
    "start_ms": 1200,
    "end_ms":   4100,
    "audio_url": "https://storage.example.com/…"
  },
  {
    "role": "model",
    "content_type": "text/plain",
    "content": "Sure, what date works best?",
    "start_ms": 4200,
    "end_ms":   6100
  },
  {
    "role": "model",
    "content_type": "application/json",
    "content": {
      "tool_call": "search_appointments",
      "arguments": { "date": "2026-04-21" }
    }
  },
  {
    "role": "tool",
    "content_type": "application/json",
    "content": {
      "tool_name": "search_appointments",
      "response": { "available_slots": ["9:00 AM", "2:00 PM"] }
    }
  }
]
الحقلالنوعالوصف
roleسلسلة نصيةuser أو model أو tool أو system
content_typeسلسلة نصيةtext/plain للكلام؛ وapplication/json لاستدعاءات الأدوات ونتائجها وأحداث النظام
contentسلسلة نصية | كائننص الكلام، أو الكائن المنظّم الموضح أعلاه. استدعاءات الأدوات: {"tool_call": name, "arguments": {…}}. نتائج الأدوات: {"tool_name": name, "response": {…}}
start_ms، end_msعدد صحيحإزاحات ابتداءً من بدء المكالمة، بالمللي ثانية. تكون موجودة عند معرفة توقيت الصوت
ttfa_msعدد صحيحالوقت حتى أول صوت لدور model، عند قياسه
audio_url، audio_urlsسلسلة نصية / مصفوفةعناوين URL موقعة منتهية الصلاحية لصوت الدور، عند تسجيل الصوت لكل دور

لاستخدام سجل الأدوار المنظّم بالكامل (مع علامات المقاطعة، وموجّهات الإقرار، والمواضع الأولية)، استخدم GET /v1/calls/{call_id}/history.

اختلافات الحمولة القديمة

غلاف خطاف الويب القديم ذي عنوان URL الواحد هو {"type": "telephony.complete" | "web.complete", "data": {…}} من دون event_id، وتختلف قيمة data فيه عن حمولة نقطة النهاية:

  • توجد مصفوفة الأدوار ضمن history، وليس transcripts (وبنفس مخطط الأدوار أعلاه).
  • مجموعة الحقول هي تقرير نهاية المكالمة الأولي، وقد تتضمن حقولًا داخلية إضافية غير المذكورة في الجدول أعلاه — تعامل مع الحقول غير المعروفة على أنها معلوماتية.
  • مكالمات الويب (direction: "web") لا تتضمن from_number / to_number وتضيف origin_domain.
  • تُبلّغ مكالمات اختبار الميكروفون في أداة الإنشاء على أنها telephony.complete عبر المسار القديم (يحوّلها نظام نقطة النهاية إلى web.complete).
  • تنسيق التحويل: عند انتهاء مكالمة بتحويل، يُستدعى خطاف الويب القديم بشكل متزامن، وقد يجيب بـ {"transfer_ready": false} للإشارة إلى أن هدف التسليم غير جاهز. يسمح أي رد آخر (أو عدم وجود خطاف ويب قديم) بمتابعة التحويل. لا تتم استشارة عمليات التسليم عبر نقطة النهاية لهذا الغرض.

مثال على معالج

Python (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, signature: str) -> bool:
    expected = hmac.new(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"] in ("telephony.complete", "web.complete"):
        data = event["data"]
        # Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
        turns = data.get("transcripts") or data.get("history") or []
        await persist_call_record(
            call_id=data["call_id"],
            turns=turns,
            recording_url=data.get("recording_url"),
        )
        if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
            await notify_team(data.get("transfer_number"), data["call_id"])
    return {"ok": True}
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" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    if (["telephony.complete", "web.complete"].includes(event.type)) {
      const data = event.data;
      // Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
      const turns = data.transcripts ?? data.history ?? [];
      await persistCallRecord({ ...data, turns });
      if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
        await notifyTeam(data.transfer_number, data.call_id);
      }
    }
    res.json({ ok: true });
  },
);

حالات الاستخدام الشائعة

تكامل CRM

احفظ النص المفرغ لكل مكالمة ورابط التسجيل إلى جانب سجلات عملائك.

التحليلات

أرسل النصوص المفرغة إلى مسار معالجة لنمذجة الموضوعات، أو استخراج مؤشرات CSAT، أو مراقبة معدل التحويل.

مراجعة الجودة

افتح المكالمات في أداة ضمان الجودة لمراجعتها بشريًا، أو مررها عبر نموذج التقييم الخاص بك.

الإشعارات

نبّه زميلًا بشريًا عند التحويل أو الإخفاق.


ذو صلة