---
title: "ตรวจสอบลายเซ็น Webhook"
description: "Webhook และคำขอเครื่องมือทุกรายการที่ ThunderPhone ส่งจะมีการลงลายเซ็น ตรวจสอบลายเซ็นหนึ่งครั้งด้วยแนวทางในหน้านี้ แล้วนำการตรวจสอบเดียวกันนี้ไปใช้ซ้ำกับทุกเอนด์พอยต์ที่คุณใช้งาน"
---

ทุกคำขอที่เราส่งไปยังเซิร์ฟเวอร์ของคุณ — ทั้งการส่ง webhook และการเรียกใช้ tool endpoint — จะมีลายเซ็น HMAC-SHA256 อยู่ในส่วนหัว `X-ThunderPhone-Signature` ตรวจสอบให้ถูกต้องเพียงครั้งเดียว แล้วใช้ตัวช่วยเดียวกันกับ handler ทุกตัว

## อัลกอริทึม

1. อ่านเนื้อหาคำขอ **แบบดิบ** — ไบต์ที่ตรงตามที่เรา POST ให้คุณทุกประการ
2. คำนวณ `hmac_sha256(secret, body).hexdigest()`
3. เปรียบเทียบกับ `X-ThunderPhone-Signature` แบบ **เวลาคงที่**
   (การเปรียบเทียบสตริงแบบทั่วไปจะเปิดเผยข้อมูลด้านเวลา)

เราลงลายเซ็นบนไบต์ที่ส่งจริงทุกประการ ดังนั้นการตรวจสอบเนื้อหาแบบดิบจึงใช้ได้เสมอ ไบต์เหล่านั้นยังเป็น **การทำให้ JSON เป็นรูปแบบมาตรฐาน**
ของ payload ด้วย — คีย์เรียงตามตัวอักษร ตัวคั่นแบบกระชับ
(`,` และ `:` โดยไม่มีช่องว่าง) และ UTF-8 ซึ่งให้วิธีที่สองที่เทียบเท่ากันโดยสมบูรณ์ เมื่อเฟรมเวิร์กของคุณเปิดเผยเฉพาะ JSON ที่แยกวิเคราะห์แล้ว:
ทำให้เป็นรูปแบบมาตรฐานอีกครั้ง แล้วคำนวณ HMAC จากผลลัพธ์นั้น

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

ควรใช้เนื้อหาแบบดิบ — มีขั้นตอนน้อยกว่า และไม่ถูกรบกวนจากลักษณะเฉพาะของการแปลงตัวเลข JSON ไปกลับในบางภาษา

## ใช้ secret ใด

| แหล่งที่มา | Secret |
|--------|--------|
| [Webhook endpoint](/th/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | `secret` เฉพาะ endpoint (อักขระ hex 48 ตัว) ที่ส่งกลับเพียงครั้งเดียวเมื่อสร้าง |
| [Webhook URL เดียวแบบเดิม](/api-reference/organizations#legacy-single-url-webhook) | `secret` เฉพาะองค์กรที่ส่งกลับจาก `GET /v1/webhook` |
| [การเรียกใช้ Tool endpoint](/th/tools/overview) (เรียกไปยัง `endpoint.url` ของคุณโดยตรง) | **webhook secret ระดับองค์กร** (ค่าเดียวกับ webhook URL เดียวแบบเดิม) — ไม่ใช่ secret เฉพาะ endpoint |

จัดเก็บ secret ไว้ในตัวจัดการ secret หรือตัวแปรสภาพแวดล้อม — ห้าม commit โดยเด็ดขาด

## ตัวอย่างการติดตั้งใช้งานอ้างอิง

ทั้งสี่ตัวอย่างตรวจสอบเนื้อหาคำขอแบบดิบ:

<CodeGroup>
```python Python
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
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
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
require "openssl"

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

## การเชื่อมต่อเฉพาะเฟรมเวิร์ก

<CodeGroup>
```python FastAPI
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
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
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>

## การตรวจสอบการเรียกใช้เครื่องมือ

เมื่อเอเจนต์เรียกใช้
[เครื่องมือฟังก์ชัน](/th/tools/overview) รายการใดรายการหนึ่งของคุณโดยตรง (เครื่องมือนั้นมี
`endpoint`) คำขอจะมีส่วนหัวของ ThunderPhone สองรายการเพิ่มเติมจาก
`endpoint.headers` ที่คุณกำหนดค่าไว้:

- `X-ThunderPhone-Call-ID` — รหัสตัวเลขของสายที่กำลังดำเนินอยู่
- `X-ThunderPhone-Signature` — HMAC-SHA256 ที่ใช้คีย์เป็น
  **ความลับเว็บฮุกระดับองค์กร** กับไบต์ของเนื้อความคำขอที่ตรงกันทุกประการ

ตัวช่วย `verify()` เดียวกันใช้งานได้โดยไม่ต้องเปลี่ยนแปลง โดยมีข้อควรทราบสองประการ:

1. เครื่องมือ **`GET` / `DELETE` ไม่มีเนื้อความ** อาร์กิวเมนต์จะส่งผ่านพารามิเตอร์คิวรี
   และลายเซ็นจะคำนวณจาก **สตริงไบต์ว่าง** — ดังนั้นใช้ `verify(b"", sig, secret)`
   (Python) หรือ `verify(Buffer.alloc(0), sig, secret)` (Node) อย่าแฮช
   สตริงคิวรี
2. **องค์กรที่ไม่ได้กำหนดค่าเว็บฮุกแบบเดิมจะไม่มีความลับระดับองค์กร** ในกรณีนั้น
   การเรียกใช้เครื่องมือจะมีเพียง `X-ThunderPhone-Call-ID` และไม่มีส่วนหัวลายเซ็น
   กำหนดค่าเว็บฮุกแบบเดิม (`PUT /v1/webhook`) เพื่อรับความลับสำหรับการลงนาม
   หรือยืนยันตัวตนการเรียกใช้เครื่องมือด้วยส่วนหัวของคุณเองผ่าน `endpoint.headers`

```python
@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)
    ...
```

การส่งต่อเครื่องมือใน**โหมดเว็บฮุก** (เครื่องมือที่ไม่มี `endpoint` ซึ่งส่งไปยัง
เว็บฮุกขององค์กรคุณเป็น `telephony.tool` / `web.tool`) คือเว็บฮุกที่ลงนามตามปกติ —
ใช้ขั้นตอนมาตรฐานข้างต้น ดู
[เครื่องมือฟังก์ชัน](/th/tools/overview) สำหรับรูปแบบคำขอทั้งสองแบบ

## ข้อผิดพลาดที่พบบ่อย

<AccordionGroup>
  <Accordion title="ซีเรียลไลซ์ซ้ำด้วยการจัดรูปแบบเริ่มต้น">
    การแยกวิเคราะห์เนื้อหาแล้วดัมพ์ซ้ำด้วยค่าเริ่มต้นของไลบรารี JSON
    (เว้นวรรคหลัง `,` / `:`, คีย์เรียงตามลำดับการแทรก) จะสร้าง
    ไบต์ที่ต่างกันและทำให้ HMAC ใช้งานไม่ได้ ตรวจสอบเนื้อหาดิบ — หรือหาก
    จำเป็นต้องซีเรียลไลซ์ซ้ำ ให้ตรงกับรูปแบบมาตรฐานของเราอย่างเคร่งครัด: คีย์เรียงลำดับ
    ตัวคั่นแบบกระชับ UTF-8
  </Accordion>

  <Accordion title="เฟรมเวิร์กแยกวิเคราะห์ JSON อัตโนมัติ">
    มิดเดิลแวร์ `express.json()` ของ Express จะใช้สตรีมเนื้อหา
    ทำให้คุณสูญเสียไบต์ดิบ ใช้ `express.raw()` กับเส้นทาง webhook
    โดยเฉพาะ หรือบัฟเฟอร์เนื้อหาดิบในมิดเดิลแวร์ก่อนหน้า
    NestJS / Koa ก็เช่นกัน — ตรวจสอบเอกสารเกี่ยวกับเนื้อหาดิบของแต่ละระบบ
  </Accordion>

  <Accordion title="การเปรียบเทียบที่ไม่ปลอดภัยต่อการจับเวลา">
    `expected === signature` ใน JS หรือ `expected == signature` ใน
    Python เป็นการเปรียบเทียบที่ใช้เวลาไม่คงที่ ให้ใช้ `crypto.timingSafeEqual`
    หรือ `hmac.compare_digest` ตามลำดับ ความแตกต่างด้านประสิทธิภาพ
    ไม่มีนัยสำคัญ
  </Accordion>

  <Accordion title="ใช้ซีเคร็ตผิดสำหรับปลายทางเครื่องมือ">
    การเรียกปลายทางเครื่องมือโดยตรงจะลงลายเซ็นด้วย **ซีเคร็ต webhook ระดับองค์กร**
    (`GET /v1/webhook`) — ไม่ใช่ซีเคร็ตเฉพาะปลายทางใด ๆ
    จาก `/v1/developer/webhook-endpoints` ใช้ฟังก์ชัน `verify()`
    เดิมซ้ำได้ แต่ต้องตรวจสอบว่าคุณส่งซีเคร็ตขององค์กรให้ฟังก์ชันในเส้นทางเครื่องมือ
  </Accordion>

  <Accordion title="แฮชสตริงคิวรีในเครื่องมือ GET/DELETE">
    สำหรับเมธอดเครื่องมือที่ไม่มีเนื้อหา ลายเซ็นจะครอบคลุม
    สตริงไบต์ว่าง เพื่อคงสูตรเดียวที่ใช้ได้ทุกกรณี: ทำ HMAC กับเนื้อหาคำขอดิบ
    ไม่ว่าจะเป็นอะไร การแฮช URL หรือสตริงคิวรีจะไม่ตรงกัน
  </Accordion>

  <Accordion title="ไม่ส่งคืน 401 เมื่อไม่ตรงกัน">
    การส่งคืน 200 เมื่อการตรวจสอบล้มเหลวทำให้ตัวจัดการเป็นเป้าหมายของการเล่นซ้ำ
    ให้ตอบกลับด้วยสถานะที่ไม่ใช่ 2xx เสมอหากการตรวจสอบล้มเหลว
  </Accordion>
</AccordionGroup>

---

## ขั้นตอนถัดไป

<CardGroup cols={2}>
  <Card title="ภาพรวม Webhook" icon="bolt" href="/th/webhooks/overview">
    ความหมายของการจัดส่ง การลองใหม่ IP ต้นทาง
  </Card>
  <Card title="ปลายทาง Webhook" icon="plug" href="/th/webhooks/endpoints">
    จัดการ URL หลายรายการ หมุนเวียนซีเคร็ต
  </Card>
  <Card title="เครื่องมือฟังก์ชัน" icon="screwdriver-wrench" href="/th/tools/overview">
    เส้นทางการเรียกใช้เครื่องมือ 2 แบบและรูปแบบคำขอ
  </Card>
  <Card title="การผสานรวมเครื่องมือ" icon="wrench" href="/th/guides/build-tool-integration">
    สร้างการผสานรวมที่รองรับเครื่องมืออย่างสมบูรณ์ตั้งแต่ต้นจนจบ
  </Card>
</CardGroup>
