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

# Xác minh chữ ký webhook

> Mọi yêu cầu webhook và công cụ từ ThunderPhone đều được ký. Xác minh một lần; tái sử dụng ở mọi nơi.

Mọi yêu cầu chúng tôi gửi đến máy chủ của bạn — lượt gửi webhook và
lệnh gọi tool endpoint — đều có chữ ký HMAC-SHA256 trong header
`X-ThunderPhone-Signature`. Xác thực đúng một lần rồi dùng cùng helper
cho mọi handler.

## Thuật toán

1. Đọc phần thân yêu cầu **thô** — chính xác các byte chúng tôi POST đến bạn.
2. Tính `hmac_sha256(secret, body).hexdigest()`.
3. So sánh theo **thời gian hằng số** với `X-ThunderPhone-Signature`.
   (So sánh chuỗi thông thường làm lộ thông tin thời gian.)

Chúng tôi ký chính xác các byte được truyền đi, vì vậy việc xác thực phần thân
thô luôn hoạt động. Các byte đó cũng là **chuỗi tuần tự hóa JSON chuẩn**
của payload — khóa được sắp xếp theo thứ tự chữ cái, dấu phân cách gọn
(`,` và `:` không có khoảng trắng), UTF-8. Điều này cung cấp cho bạn một cách
thứ hai hoàn toàn tương đương khi framework của bạn chỉ cung cấp JSON đã phân tích:
tuần tự hóa lại theo chuẩn rồi tính HMAC cho dữ liệu đó.

```python theme={null}
# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
```

Ưu tiên phần thân thô — ít hơn một bước và tránh được các vấn đề khi chuyển đổi
qua lại số JSON trong một số ngôn ngữ.

## Dùng secret nào?

| Nguồn                                                                                   | Secret                                                                                                          |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [Webhook endpoint](/vi/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)          | `secret` riêng cho từng endpoint (48 ký tự hex) chỉ được trả về một lần khi tạo                                 |
| [Webhook URL đơn lẻ cũ](/api-reference/organizations#legacy-single-url-webhook)         | `secret` riêng cho từng org được trả về qua `GET /v1/webhook`                                                   |
| [Lệnh gọi tool endpoint](/vi/tools/overview) (gọi trực tiếp đến `endpoint.url` của bạn) | **Webhook secret cấp org** (giống secret của webhook URL đơn lẻ cũ) — không phải secret riêng cho từng endpoint |

Lưu secret trong trình quản lý secret hoặc biến môi trường — không bao giờ commit secret đó.

## Các cách triển khai tham khảo

Cả bốn cách đều xác thực phần thân yêu cầu thô:

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac


  def verify(body: bytes, signature: str, secret: str) -> bool:
      """Constant-time HMAC-SHA256 verification."""
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature or "")
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  export function verify(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),
    );
  }
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
  )

  func Verify(body []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(expected), []byte(signature))
  }
  ```

  ```ruby Ruby theme={null}
  require "openssl"

  def verify(body, signature, secret)
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
    Rack::Utils.secure_compare(expected, signature.to_s)
  end
  ```
</CodeGroup>

## Kết nối theo framework cụ thể

<CodeGroup>
  ```python FastAPI theme={null}
  from fastapi import FastAPI, HTTPException, Request

  app = FastAPI()

  @app.post("/thunderphone-webhook")
  async def hook(request: Request):
      body = await request.body()           # raw bytes, NOT request.json()
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          raise HTTPException(status_code=401)

      import json
      event = json.loads(body)
      # … dispatch on event["type"] …
      return {"ok": True}
  ```

  ```javascript Express theme={null}
  import express from "express";

  const app = express();

  app.post(
    "/thunderphone-webhook",
    // IMPORTANT: parse as raw; do NOT use express.json() here.
    express.raw({ type: "application/json" }),
    (req, res) => {
      const sig = req.header("X-ThunderPhone-Signature") || "";
      if (!verify(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);
    },
  );
  ```

  ```python Django theme={null}
  import json

  from django.http import JsonResponse, HttpResponseForbidden
  from django.views.decorators.csrf import csrf_exempt
  from django.views.decorators.http import require_POST


  @csrf_exempt
  @require_POST
  def hook(request):
      body = request.body  # raw bytes
      sig = request.headers.get("X-ThunderPhone-Signature", "")
      if not verify(body, sig, SECRET):
          return HttpResponseForbidden("invalid signature")
      event = json.loads(body)
      # … dispatch on event["type"] …
      return JsonResponse({"ok": True})
  ```
</CodeGroup>

## Xác minh lệnh gọi công cụ

Khi tác nhân AI gọi trực tiếp một trong các
[công cụ hàm](/vi/tools/overview) của bạn (công cụ có
`endpoint`), yêu cầu sẽ mang hai header ThunderPhone bên cạnh
`endpoint.headers` bạn đã cấu hình:

* `X-ThunderPhone-Call-ID` — id dạng số của cuộc gọi đang diễn ra.
* `X-ThunderPhone-Signature` — HMAC-SHA256, sử dụng
  **secret webhook cấp tổ chức** của bạn làm khóa, trên chính xác các byte của phần thân yêu cầu.

Cùng helper `verify()` hoạt động không thay đổi, với hai điểm cần lưu ý:

1. **Công cụ `GET` / `DELETE` không có phần thân.** Đối số được truyền dưới dạng tham số truy vấn
   và chữ ký được tính trên **chuỗi byte rỗng**
   — do đó dùng `verify(b"", sig, secret)` (Python) hoặc
   `verify(Buffer.alloc(0), sig, secret)` (Node). **Không** hash
   chuỗi truy vấn.
2. **Tổ chức không cấu hình webhook cũ sẽ không có secret cấp tổ chức.** Trong
   trường hợp đó, lệnh gọi công cụ chỉ mang `X-ThunderPhone-Call-ID` và không có
   header chữ ký. Cấu hình webhook cũ
   (`PUT /v1/webhook`) để có secret ký, hoặc xác thực lệnh gọi công cụ
   bằng header riêng của bạn qua `endpoint.headers`.

```python theme={null}
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...
```

Điều phối công cụ ở chế độ webhook-**mode** (công cụ không có `endpoint`, được gửi
đến webhook tổ chức của bạn dưới dạng `telephony.tool` / `web.tool`) là một webhook
được ký thông thường — áp dụng quy trình chuẩn ở trên. Xem
[Công cụ hàm](/vi/tools/overview) để biết cả hai dạng yêu cầu.

## Các lỗi thường gặp

<AccordionGroup>
  <Accordion title="Tuần tự hóa lại với định dạng mặc định">
    Phân tích body rồi xuất lại bằng các thiết lập mặc định của thư viện JSON
    (dấu cách sau `,` / `:`, khóa theo thứ tự chèn) sẽ tạo ra
    byte khác và làm HMAC không hợp lệ. Xác minh body thô — hoặc nếu
    bắt buộc phải tuần tự hóa lại, hãy khớp chính xác định dạng chuẩn của chúng tôi: khóa được sắp xếp,
    dấu phân cách gọn, UTF-8.
  </Accordion>

  <Accordion title="Framework tự động phân tích JSON">
    Middleware `express.json()` của Express tiêu thụ luồng body
    khiến bạn mất các byte thô. Dùng `express.raw()` riêng cho route webhook,
    hoặc đệm body thô trong một middleware tiền xử lý.
    NestJS / Koa cũng tương tự — hãy xem tài liệu về "raw body" của chúng.
  </Accordion>

  <Accordion title="So sánh không an toàn theo thời gian">
    `expected === signature` trong JS hoặc `expected == signature` trong
    Python là các phép so sánh có thời gian thay đổi. Hãy dùng `crypto.timingSafeEqual`
    hoặc `hmac.compare_digest` tương ứng. Chênh lệch hiệu năng
    là không đáng kể.
  </Accordion>

  <Accordion title="Dùng sai secret cho endpoint công cụ">
    Các lệnh gọi trực tiếp đến endpoint công cụ được ký bằng **webhook
    secret cấp tổ chức** (`GET /v1/webhook`) — không phải bằng bất kỳ secret riêng theo endpoint nào
    từ `/v1/developer/webhook-endpoints`. Dùng lại cùng hàm `verify()`
    nhưng hãy đảm bảo truyền secret của tổ chức vào hàm đó trên các route công cụ.
  </Accordion>

  <Accordion title="Băm chuỗi truy vấn trên công cụ GET/DELETE">
    Với các phương thức công cụ không có body, chữ ký bao phủ chuỗi byte
    rỗng, giúp duy trì một công thức chung: HMAC body yêu cầu thô,
    bất kể đó là gì. Băm URL hoặc chuỗi truy vấn sẽ không bao giờ khớp.
  </Accordion>

  <Accordion title="Không trả về 401 khi không khớp">
    Trả về 200 khi xác minh thất bại khiến handler trở thành mục tiêu
    phát lại. Luôn phản hồi mã không thuộc nhóm 2xx nếu xác minh thất bại.
  </Accordion>
</AccordionGroup>

***

## Bước tiếp theo

<CardGroup cols={2}>
  <Card title="Tổng quan về webhook" icon="bolt" href="/vi/webhooks/overview">
    Ngữ nghĩa phân phối, thử lại, IP nguồn.
  </Card>

  <Card title="Endpoint webhook" icon="plug" href="/vi/webhooks/endpoints">
    Quản lý nhiều URL, xoay vòng secret.
  </Card>

  <Card title="Công cụ hàm" icon="screwdriver-wrench" href="/vi/tools/overview">
    Hai luồng gọi công cụ và cấu trúc yêu cầu của chúng.
  </Card>

  <Card title="Tích hợp công cụ" icon="wrench" href="/vi/guides/build-tool-integration">
    Xây dựng một tích hợp hoàn chỉnh sử dụng công cụ từ đầu đến cuối.
  </Card>
</CardGroup>
