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

Webhooks

Tổng quan về Webhooks

Cách ThunderPhone gửi sự kiện theo thời gian thực, cách xác minh chữ ký và cách so sánh các mô hình gửi phiên bản cũ và dựa trên endpoint.

ThunderPhone gửi yêu cầu HTTP POST đến máy chủ của bạn khi có sự kiện xảy ra trong cuộc gọi — cuộc gọi đến bắt đầu, cuộc gọi kết thúc, lần chạy chấm điểm hoàn tất, cảnh báo được kích hoạt, v.v. Có hai mô hình gửi:

Cả mười loại sự kiện trong danh mục sự kiện đều được gửi qua endpoint webhook. Sáu sự kiện trong vòng đời cuộc gọi (telephony.incoming, telephony.complete, telephony.tool, web.incoming, web.complete, web.tool) cũng được gửi đến webhook cũ một URL — nếu bạn có cả URL cũ và endpoint khớp, bạn sẽ nhận sự kiện trên cả hai đường dẫn. Hành vi chặn (bao gồm trao đổi cấu hình telephony.incoming / web.incomingđiều phối công cụ ở chế độ webhook) chỉ tồn tại trên đường dẫn cũ; mọi lần gửi đến endpoint đều là thông báo gửi đi mà không chờ phản hồi.

Định dạng payload

Payload gửi đến endpoint là một đối tượng JSON có data, event_idtype:

{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}

event_id là duy nhất cho mỗi sự kiện được phát ra. Giá trị này giống nhau giữa các lần thử lại giữa mọi endpoint nhận sự kiện — hãy khử trùng lặp dựa trên giá trị này.

Webhook cũ một URL gửi cùng typedata nhưng không có event_id:

{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}

Trên đường truyền, mọi body đều được tuần tự hóa theo chuẩn — các khóa được sắp xếp theo thứ tự chữ cái, không có khoảng trắng, UTF-8. Các ví dụ được định dạng đẹp trong tài liệu này chỉ nhằm mục đích dễ đọc.

Xem Danh mục sự kiện để biết danh sách đầy đủ các loại sự kiện và trường payload.

Xác minh chữ ký

Mọi yêu cầu đều kèm chữ ký HMAC-SHA256 của nội dung yêu cầu thô trong header X-ThunderPhone-Signature. Khóa ký là secret của endpoint (hoặc secret webhook cấp tổ chức của bạn đối với các lần gửi cũ).

Các bước

  1. Đọc nội dung yêu cầu thô trước khi thực hiện bất kỳ phân tích cú pháp nào.
  2. Tính hmac_sha256(secret, body).hexdigest().
  3. So sánh theo thời gian hằng với header X-ThunderPhone-Signature.

Chúng tôi ký chính xác các byte được truyền đi, và các byte đó là bản tuần tự hóa JSON chuẩn (khóa được sắp xếp, dấu phân cách gọn). Vì vậy, xác minh dựa trên nội dung thô luôn hoạt động — và nếu framework của bạn chỉ cung cấp JSON đã được phân tích cú pháp, việc tuần tự hóa lại với khóa được sắp xếp và dấu phân cách gọn sẽ tạo ra các byte giống hệt. Cả hai cách đều được trình bày trong hướng dẫn xác minh.

Python
import hmac
import hashlib
 
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
 
# Example Flask handler
from flask import Flask, request, abort
app = Flask(__name__)
 
@app.post("/thunderphone-webhook")
def handle():
    body = request.get_data()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify_signature(body, sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
 
function verifySignature(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
 
const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type …
    res.sendStatus(204);
  },
);

Ngữ nghĩa phân phối

Các ngữ nghĩa này áp dụng cho việc phân phối đến endpoint. Webhook URL đơn lẻ cũ là một lần thử đồng bộ duy nhất, không có lần thử lại.

Thử lại

Mỗi sự kiện được thử ngay một lần. Bất kỳ phản hồi 2xx nào cũng xác nhận việc phân phối. Với mọi kết quả khác (không phải 2xx, lỗi kết nối, hết thời gian chờ), chúng tôi thử lại sau 1 phút, 5 phút, 30 phút, 2 giờ, 6 giờ, 12 giờ và 24 giờ kể từ lần thử đầu tiên — 8 lần thử trong 24 giờ. Nếu mọi lần thử đều thất bại, việc phân phối dừng lại và endpoint được đánh dấu status="failing" trong các endpoint webhook. Trả về 2xx ngay khi payload đã được chấp nhận bền vững; xử lý không đồng bộ.

Thứ tự

Thứ tự phân phối được thực hiện theo nỗ lực tối đa. Trên thực tế, chúng tôi phân phối theo thứ tự sự kiện được phát ra, nhưng việc thử lại có thể đảo thứ tự khi xảy ra lỗi. Luôn khử trùng lặp và đối soát theo call_id / id đối tượng.

Bản sao

Phân phối là ít nhất một lần: việc thử lại sau một phản hồi mà chúng tôi không nhận được có thể tạo bản sao của một sự kiện. Mọi lần thử lại đều mang cùng event_id, vì vậy hãy lưu các id đã xử lý và bỏ qua các lần lặp lại. event_id cũng được dùng chung giữa các endpoint — hai endpoint đăng ký cùng một sự kiện sẽ nhận cùng event_id.

Thời gian chờ

Mỗi lần thử phân phối đến endpoint có thời gian chờ 30 giây. Trên đường dẫn cũ, các yêu cầu chặn điều khiển hành vi cuộc gọi trực tiếp — trao đổi cấu hình telephony.incoming / web.incoming — hết thời gian chờ sau 10 giây, nhưng phản hồi chậm sẽ trì hoãn việc nhận cuộc gọi, vì vậy hãy cố gắng phản hồi trong vài giây. Điều phối công cụ ở chế độ webhook cho phép 20 giây theo mặc định, và khai báo công cụ có thể đặt timeout ở cấp cao nhất.

IP nguồn

Webhook gửi đi bắt nguồn từ dải IP đám mây của ThunderPhone. Nếu tường lửa của bạn yêu cầu danh sách cho phép, hãy liên hệ bộ phận hỗ trợ và chúng tôi sẽ chia sẻ các dải hiện tại.

Lựa chọn giữa webhook cũ và webhook dựa trên endpoint

Tính năngCũ (/v1/webhook)Endpoint (/v1/developer/webhook-endpoints)
Số lượng URL1 mỗi tổ chứcNhiều mỗi tổ chức
Phạm vi sự kiệnChỉ telephony.* / web.*Cả 10 loại sự kiện
Bộ lọc sự kiệnTheo từng endpoint
Thử lạiKhông có8 lần thử trong 24 giờ
Envelopetype + datatype + data + event_id
Xoay vòng secretThay thế secret duy nhấtSecret theo từng endpoint
Vô hiệu hóa mà không xóastatus=disabled
Hiển thị trạng tháiactive / disabled / failing
Trao đổi cấu hình chặnCó (telephony.incoming / web.incoming)Không bao giờ — chỉ thông báo
Phù hợp nhất choCấu hình cuộc gọi độngTiêu thụ sự kiện trong môi trường production

Các tích hợp mới nên tiêu thụ sự kiện qua webhook dựa trên endpoint. Chỉ giữ (hoặc thêm) URL cũ nếu bạn cấu hình cuộc gọi động tại thời điểm nhận cuộc gọi hoặc sử dụng điều phối công cụ ở chế độ webhook — các trao đổi yêu cầu/phản hồi đó chỉ chạy trên đường dẫn cũ.


Liên quan