---
title: "Tổng quan về Webhook"
description: "Cách ThunderPhone gửi sự kiện theo thời gian thực, cách xác minh chữ ký và so sánh mô hình gửi cũ với mô hình gửi dựa trên endpoint."
---

ThunderPhone gửi yêu cầu HTTP `POST` đến máy chủ của bạn khi cá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, một lần chấm điểm
hoàn tất, một cảnh báo được kích hoạt, v.v. Có **hai mô hình
phân phối**:

<CardGroup cols={2}>
  <Card title="Điểm cuối webhook (khuyến nghị)" icon="bolt" href="/vi/webhooks/endpoints">
    Nhiều URL, secret riêng cho từng điểm cuối, bộ lọc sự kiện riêng cho từng điểm cuối,
    và tự động thử lại.
    Quản lý qua `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints`.
  </Card>
  <Card title="Webhook kế thừa một URL" icon="link" href="/api-reference/organizations#legacy-single-url-webhook">
    Một URL cho mỗi tổ chức. Chứa các sự kiện trong vòng đời cuộc gọi, bao gồm cả các
    trao đổi cấu hình **chặn**. Quản lý tại `GET/PUT /v1/webhook`.
  </Card>
</CardGroup>

Mọi loại sự kiện được liệt kê trong [danh mục sự kiện](/vi/webhooks/events) đều
được gửi qua điểm cuối 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 kế thừa một URL — nếu bạn có cả URL kế thừa và một điểm cuối
khớp, bạn sẽ nhận sự kiện trên **cả hai** đường dẫn. Hành vi chặn (gồm
[trao đổi cấu hình `telephony.incoming` / `web.incoming`](/vi/webhooks/call-incoming)
và [điều phối công cụ](/vi/tools/overview) ở chế độ webhook)
chỉ có trên đường dẫn kế thừa; mọi lần gửi đến điểm cuối đều là thông báo
gửi đi mà không chờ phản hồi.

## Định dạng payload

Các lần gửi đến điểm cuối là một đối tượng JSON có `data`, `event_id` và
`type`:

```json
{
  "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 hệt giữa các lần thử lại
**và** giữa mọi điểm cuối nhận sự kiện — hãy dùng giá trị này để loại bỏ trùng lặp.

Webhook kế thừa một URL gửi cùng `type` và `data` nhưng
**không có** `event_id`:

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

Trên đường truyền, mọi phần thân đều được tuần tự hóa theo chuẩn — các khóa được sắp xếp
theo thứ tự bảng 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](/vi/webhooks/events) để 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 mang hai chữ ký, cả hai đều dùng HMAC-SHA256 với khóa 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ũ):

- `X-ThunderPhone-Signature-V2: t=<unix seconds>,v1=<hex>` ký dấu thời gian,
  phương thức, đường dẫn và truy vấn, cùng phần thân thô. Xác minh chữ ký này và từ chối các
  yêu cầu có `t` chênh lệch quá **5 phút** so với đồng hồ của bạn để yêu cầu bị chặn không thể
  bị phát lại sau đó. Các lần thử lại được ký lại khi gửi.
- `X-ThunderPhone-Signature` chỉ ký phần thân thô. Chữ ký này vẫn được gửi, không thay đổi,
  để các trình xác minh hiện có tiếp tục hoạt động.

### Các bước

1. Đọc header, phương thức, đường dẫn và truy vấn thô, cùng phần
   body thô **trước khi** thực hiện bất kỳ quá trình phân tích nào.
2. Tính HMAC-SHA256 dạng hex của
   `<t>\n<METHOD>\n<path and query>\n<body>` bằng secret của bạn.
3. Từ chối yêu cầu nếu `t` lệch quá 300 giây hoặc nếu kết quả không
   khớp với `v1` trong phép so sánh thời gian hằng số.

Body 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à chúng tôi ký chính xác các byte được truyền đi. [Hướng dẫn xác minh](/vi/guides/verify-webhook-signatures) có chuỗi đã ký chính xác, ví dụ hoàn chỉnh, mã không dùng SDK và header cũ.

<CodeGroup>
```python Python (Flask)
from flask import Flask, abort, request
from thunderphone import verify_signature

app = Flask(__name__)

@app.post("/thunderphone-webhook")
def handle():
    query = request.query_string.decode()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=WEBHOOK_SECRET,
        method=request.method,
        path=request.path + (f"?{query}" if query else ""),
        body=request.get_data(),  # raw bytes
    ):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
```

```javascript Node.js (Express)
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl,
      body: req.body,
    });
    if (!ok) return res.sendStatus(401);
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type …
    res.sendStatus(204);
  },
);
```
</CodeGroup>

## Ngữ nghĩa phân phối

Các ngữ nghĩa này áp dụng cho lần gử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, ngoại trừ
trao đổi cấu hình [`telephony.incoming`](/vi/webhooks/call-incoming)
cho cuộc gọi điện thoại, được thử lại một lần.

<AccordionGroup>
  <Accordion title="Thử lại">
    Mỗi sự kiện được thử ngay lập tức một lần. Mọi phản hồi `2xx`
    đều xác nhận lần gử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 gửi sẽ dừng và endpoint
    được đánh dấu `status="failing"` trong
    [endpoint webhook](/vi/webhooks/endpoints); chúng tôi gửi email cho
    chủ sở hữu và quản trị viên của tổ chức bạn, đồng thời hiển thị thông tin này trên bảng điều khiển. Trả về `2xx` ngay khi
    payload được chấp nhận và lưu bền vững; xử lý không đồng bộ.
    Lần gửi kiểm thử từ API kiểm thử endpoint được thử lại một lần khoảng một phút sau
    lần thử đầu tiên và không bao giờ thay đổi trạng thái của endpoint.
  </Accordion>

  <Accordion title="Thứ tự">
    Thứ tự gửi là theo nỗ lực tốt nhất. Trên thực tế, chúng tôi gửi theo thứ tự sự kiện được phát ra, nhưng việc thử lại có thể làm thay đổi 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.
  </Accordion>

  <Accordion title="Bản sao">
    Việc gửi là **ít nhất một lần**: một lần thử lại sau phản hồi mà chúng tôi không nhận được có thể tạo bản sao của sự kiện. Mỗi lần thử lại 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`.
  </Accordion>

  <Accordion title="Thời gian chờ">
    Lần gửi đến endpoint có thời gian chờ **30 giây** cho mỗi lần thử. Trên
    đường dẫn cũ, các yêu cầu chặn chi phối hành vi cuộc gọi trực tiếp — trao đổi
    cấu hình [`telephony.incoming` / `web.incoming`](/vi/webhooks/call-incoming)
    — bị giới hạn ở **10 giây** cho lần thử đầu tiên của một cuộc gọi điện thoại
    và **13 giây** cho toàn bộ quá trình trao đổi, bao gồm một lần thử lại,
    và ở **10 giây** cho một phiên widget. Phản hồi chậm sẽ làm chậm
    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ụ](/vi/tools/overview) ở chế độ webhook
    mặc định cho phép 20 giây và khai báo công cụ có thể đặt `timeout` ở cấp cao nhất.
  </Accordion>

  <Accordion title="IP nguồn">
    Webhook gửi đi xuất phát 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.
  </Accordion>
</AccordionGroup>

## Chọn giữa webhook cũ và webhook dựa trên endpoint

| Tính năng | Phiên bản cũ (`/v1/webhook`) | Endpoint (`/v1/developer/webhook-endpoints`) |
|---------|------------------------|----------------------------------------------|
| Số lượng URL | 1 cho mỗi tổ chức | Nhiều cho 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ó, trừ một lần cho quá trình trao đổi cấu hình cuộc gọi điện thoại | 8 lần thử trong 24 giờ |
| Bao bọc | `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 không cần xóa | `PUT /v1/webhook` với `{"url": ""}` | `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`](/vi/webhooks/call-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ữ lại (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

<CardGroup cols={2}>
  <Card title="Danh mục sự kiện" icon="list" href="/vi/webhooks/events">
    Tất cả loại sự kiện và payload của chúng.
  </Card>
  <Card title="Điểm cuối webhook" icon="bolt" href="/vi/webhooks/endpoints">
    Quản lý nhiều điểm cuối, bộ lọc sự kiện và bí mật.
  </Card>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/vi/webhooks/call-incoming">
    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.
  </Card>
  <Card title="telephony.complete / web.complete" icon="phone" href="/vi/webhooks/call-complete">
    Payload sau cuộc gọi có bản chép lời, bản ghi âm và chỉ số.
  </Card>
</CardGroup>
