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

# Verifikasi tanda tangan webhook

> Setiap permintaan webhook dan alat dari ThunderPhone ditandatangani. Verifikasi sekali; gunakan kembali di mana saja.

Setiap permintaan yang kami kirim ke server Anda — pengiriman webhook dan
pemanggilan endpoint tool — membawa tanda tangan HMAC-SHA256 dalam header
`X-ThunderPhone-Signature`. Lakukan verifikasi dengan benar sekali, lalu
gunakan helper yang sama di setiap handler.

## Algoritme

1. Baca body permintaan **mentah** — byte persis yang kami POST kepada Anda.
2. Hitung `hmac_sha256(secret, body).hexdigest()`.
3. Bandingkan dalam **waktu konstan** dengan `X-ThunderPhone-Signature`.
   (Perbandingan string naif membocorkan informasi waktu.)

Kami menandatangani byte persis yang kami kirimkan, sehingga memverifikasi body
mentah selalu berfungsi. Byte tersebut juga merupakan **serialisasi JSON kanonis**
dari payload — kunci diurutkan secara alfabetis, pemisah ringkas
(`,` dan `:` tanpa spasi), UTF-8. Ini memberi Anda resep kedua yang sepenuhnya
setara ketika framework Anda hanya menyediakan JSON yang sudah diurai:
serialisasikan ulang secara kanonis dan terapkan HMAC pada hasilnya.

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

Utamakan body mentah — ini mengurangi satu langkah dan kebal terhadap keanehan
konversi bolak-balik angka JSON dalam beberapa bahasa.

## Secret yang mana?

| Sumber                                                                                      | Secret                                                                                                     |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| [Endpoint webhook](/id/webhooks/endpoints) (`/v1/developer/webhook-endpoints`)              | `secret` per endpoint (48 karakter hex) yang dikembalikan satu kali saat dibuat                            |
| [Webhook URL tunggal legacy](/api-reference/organizations#legacy-single-url-webhook)        | `secret` per organisasi yang dikembalikan pada `GET /v1/webhook`                                           |
| [Pemanggilan endpoint tool](/id/tools/overview) (panggilan langsung ke `endpoint.url` Anda) | **secret webhook tingkat organisasi** (sama dengan webhook URL tunggal legacy) — bukan secret per endpoint |

Simpan secret di pengelola secret atau variabel lingkungan Anda — jangan pernah melakukan commit.

## Implementasi referensi

Keempat implementasi berikut memverifikasi body permintaan mentah:

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

## Pengkabelan khusus framework

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

## Memverifikasi panggilan tool

Saat agen memanggil salah satu
[function tool](/id/tools/overview) Anda secara langsung (tool tersebut memiliki
`endpoint`), permintaan membawa dua header ThunderPhone berikut bersama
`endpoint.headers` yang Anda konfigurasi:

* `X-ThunderPhone-Call-ID` — ID numerik panggilan yang sedang berlangsung.
* `X-ThunderPhone-Signature` — HMAC-SHA256, dengan kunci berupa
  **secret webhook tingkat organisasi Anda**, atas byte isi permintaan yang persis sama.

Helper `verify()` yang sama dapat digunakan tanpa perubahan, dengan dua perbedaan:

1. **Tool `GET` / `DELETE` tidak memiliki isi.** Argumen dikirim sebagai parameter
   kueri, dan tanda tangan dihitung atas **string byte kosong** —
   sehingga gunakan `verify(b"", sig, secret)` (Python) atau
   `verify(Buffer.alloc(0), sig, secret)` (Node). Jangan melakukan hash pada
   string kueri.
2. **Organisasi tanpa webhook legacy yang dikonfigurasi tidak memiliki secret organisasi.** Dalam
   kasus tersebut, panggilan tool hanya membawa `X-ThunderPhone-Call-ID` dan tidak ada
   header tanda tangan. Konfigurasikan webhook legacy
   (`PUT /v1/webhook`) untuk mendapatkan secret penandatanganan, atau autentikasi panggilan tool
   dengan header Anda sendiri melalui `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)
    ...
```

Pengiriman tool mode-**webhook** (tool tanpa `endpoint`, dikirimkan
ke webhook organisasi Anda sebagai `telephony.tool` / `web.tool`) adalah webhook bertanda tangan biasa —
resep standar di atas berlaku. Lihat
[Function Tools](/id/tools/overview) untuk kedua bentuk permintaan.

## Kesalahan umum

<AccordionGroup>
  <Accordion title="Serialisasi ulang dengan pemformatan default">
    Mengurai body lalu melakukan dump ulang dengan pengaturan default
    library JSON Anda (spasi setelah `,` / `:`, kunci berurutan berdasarkan penyisipan) menghasilkan
    byte yang berbeda dan merusak HMAC. Verifikasi body mentah — atau jika
    Anda harus melakukan serialisasi ulang, samakan persis dengan bentuk kanonis kami: kunci
    diurutkan, pemisah ringkas, UTF-8.
  </Accordion>

  <Accordion title="Framework mengurai JSON secara otomatis">
    Middleware `express.json()` Express menggunakan stream body
    sehingga Anda kehilangan byte mentah. Gunakan `express.raw()` khusus pada rute
    webhook, atau buffer body mentah dalam pre-middleware.
    Hal yang sama berlaku untuk NestJS / Koa — periksa dokumentasi "raw body" mereka.
  </Accordion>

  <Accordion title="Perbandingan yang tidak aman terhadap timing">
    `expected === signature` di JS atau `expected == signature` di
    Python adalah perbandingan dengan waktu yang bervariasi. Gunakan `crypto.timingSafeEqual`
    atau `hmac.compare_digest` secara berurutan. Perbedaan performanya
    tidak ada.
  </Accordion>

  <Accordion title="Secret yang salah untuk endpoint tool">
    Panggilan endpoint tool langsung ditandatangani dengan **secret webhook
    tingkat organisasi** (`GET /v1/webhook`) — bukan dengan secret per-endpoint
    apa pun dari `/v1/developer/webhook-endpoints`. Gunakan kembali fungsi `verify()`
    yang sama, tetapi pastikan Anda memberinya secret organisasi pada rute tool.
  </Accordion>

  <Accordion title="Melakukan hash pada query string untuk tool GET/DELETE">
    Untuk metode tool tanpa body, tanda tangan mencakup string byte
    kosong, sehingga satu resep universal tetap digunakan: HMAC body permintaan mentah,
    apa pun isinya. Melakukan hash pada URL atau query string tidak akan pernah cocok.
  </Accordion>

  <Accordion title="Tidak mengembalikan 401 saat tidak cocok">
    Mengembalikan 200 saat verifikasi gagal menjadikan handler sebagai
    target replay. Selalu respons dengan non-2xx jika verifikasi gagal.
  </Accordion>
</AccordionGroup>

***

## Langkah berikutnya

<CardGroup cols={2}>
  <Card title="Ringkasan webhook" icon="bolt" href="/id/webhooks/overview">
    Semantik pengiriman, percobaan ulang, IP sumber.
  </Card>

  <Card title="Endpoint webhook" icon="plug" href="/id/webhooks/endpoints">
    Kelola beberapa URL, rotasi secret.
  </Card>

  <Card title="Function Tools" icon="screwdriver-wrench" href="/id/tools/overview">
    Dua jalur pemanggilan tool dan bentuk permintaannya.
  </Card>

  <Card title="Integrasi tool" icon="wrench" href="/id/guides/build-tool-integration">
    Buat integrasi lengkap yang didukung tool dari awal hingga akhir.
  </Card>
</CardGroup>
