> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tổng quan về webhook

> 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 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ó sự kiện
xảy ra trong một cuộc gọi — một cuộc gọi đến bắt đầu, một cuộc gọi kết thúc, một lượt
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
gửi**:

<CardGroup cols={2}>
  <Card title="Endpoint webhook (khuyến nghị)" icon="bolt" href="/vi/webhooks/endpoints">
    Nhiều URL, secret cho từng endpoint, bộ lọc sự kiện cho từng endpoint,
    và tự động thử lại.
    Quản lý qua `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints`.
  </Card>

  <Card title="Webhook cũ 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 trao đổi cấu hình **chặn**. Quản lý tại `GET/PUT /v1/webhook`.
  </Card>
</CardGroup>

Cả mười loại sự kiện trong [danh mục sự kiện](/vi/webhooks/events) đề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à một 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`](/vi/webhooks/call-incoming)
và [điều phối công cụ](/vi/tools/overview) ở chế độ webhook)
chỉ có trên đường dẫn cũ; mọi lần gửi đến endpoint đều là thông báo gửi đi
không chờ phản hồi.

## Định dạng payload

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

```json theme={null}
{
  "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 sinh. Giá trị này giống nhau
trong các lần thử lại **và** trên 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`:

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

Khi truyền đi, mọi body được tuần tự hóa theo chuẩn tắc — 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](/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 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 cho các lần gửi
cũ).

### Các bước

1. Đọc nội dung yêu cầu thô **trước** khi phân tích cú pháp.
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,
việc 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](/vi/guides/verify-webhook-signatures).

<CodeGroup>
  ```python Python theme={null}
  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
  ```

  ```javascript Node.js (Express) theme={null}
  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);
    },
  );
  ```
</CodeGroup>

## 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.

<AccordionGroup>
  <Accordion title="Thử lại">
    Mỗi sự kiện được thử gửi ngay một lần. Mọi phản hồi `2xx`
    đều xác nhận việc phân phối. Với bất kỳ kết quả nào khác (không phải 2xx,
    lỗi kết nối, hết thời gian chờ), chúng tôi sẽ 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 sẽ dừng và endpoint
    được đánh dấu `status="failing"` trong
    [webhook endpoint](/vi/webhooks/endpoints). Hãy trả về `2xx` ngay khi
    payload được chấp nhận và lưu bền vững; xử lý bất đồng bộ.
  </Accordion>

  <Accordion title="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ự các 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">
    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 sự kiện. Mỗi lần thử lại mang cùng
    `event_id`, vì vậy hãy lưu id đã xử lý và bỏ qua các lần lặp. `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ờ">
    Việc phân phố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 đ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`](/vi/webhooks/call-incoming) —
    sẽ 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ụ](/vi/tools/overview)
    ở chế độ webhook cho phép 20 giây.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

## 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`                 |
| Luân phiên 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`](/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ếp nhận sự kiện thông 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="Webhook endpoint" icon="bolt" href="/vi/webhooks/endpoints">
    Quản lý nhiều endpoint, bộ lọc sự kiện và secret.
  </Card>

  <Card title="telephony.incoming / web.incoming" icon="phone" href="/vi/webhooks/call-incoming">
    Yêu cầu chặn mà server 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 kèm bản chép lời, bản ghi âm và chỉ số.
  </Card>
</CardGroup>
