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:
Nhiều URL, secret riêng cho từng endpoint, bộ lọc sự kiện riêng cho từng endpoint,
và tự động thử lại.
Quản lý qua GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints.
Một URL cho mỗi tổ chức. Mang theo các sự kiện trong vòng đời cuộc gọi, bao gồm
các trao đổi cấu hình chặn. Quản lý tại GET/PUT /v1/webhook.
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 và
đ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_id và
type:
{
"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
và 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 type và data 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
- Đọ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.
- Tính
hmac_sha256(secret, body).hexdigest(). - 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.
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 "", 204import 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ăng | Cũ (/v1/webhook) | Endpoint (/v1/developer/webhook-endpoints) |
|---|---|---|
| Số lượng URL | 1 mỗi tổ chức | Nhiều mỗi tổ chức |
| Phạm vi sự kiện | Chỉ telephony.* / web.* | Cả 10 loại sự kiện |
| Bộ lọc sự kiện | — | Theo từng endpoint |
| Thử lại | Không có | 8 lần thử trong 24 giờ |
| Envelope | type + data | type + data + event_id |
| Xoay vòng secret | Thay thế secret duy nhất | Secret theo từng endpoint |
| Vô hiệu hóa mà không xóa | — | status=disabled |
| Hiển thị trạng thái | — | active / disabled / failing |
| Trao đổi cấu hình chặn | Có (telephony.incoming / web.incoming) | Không bao giờ — chỉ thông báo |
| Phù hợp nhất cho | Cấu hình cuộc gọi động | Tiê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
Tất cả loại sự kiện và payload của chúng.
Quản lý nhiều endpoint, bộ lọc sự kiện và secret.
Yêu cầu chặn mà máy chủ của bạn phải phản hồi để cấu hình cuộc gọi.
Payload sau cuộc gọi gồm bản chép lời, bản ghi âm và chỉ số.