ThunderPhone 2.0 kini resmi hadir.Layanan mandiri, mulai dari 2¢/menit.Baca pengumumannya

Operations

Verifikasi tanda tangan webhook

Setiap permintaan webhook dan alat yang dikirim ThunderPhone ditandatangani. Verifikasi tanda tangan sekali menggunakan panduan di sini, lalu gunakan kembali pemeriksaan yang sama pada setiap endpoint yang Anda jalankan.

Setiap permintaan yang kami kirim ke server Anda — pengiriman webhook dan pemanggilan endpoint alat — membawa tanda tangan HMAC-SHA256 di 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 lakukan HMAC terhadapnya.

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

Utamakan body mentah — langkahnya lebih sedikit dan kebal terhadap keunikan round-trip angka JSON dalam beberapa bahasa.

Rahasia yang mana?

SumberRahasia
Endpoint webhook (/v1/developer/webhook-endpoints)secret per endpoint (48 karakter heksadesimal) yang dikembalikan sekali saat pembuatan
Webhook URL tunggal lamasecret per organisasi yang dikembalikan pada GET /v1/webhook
Pemanggilan endpoint alat (panggilan langsung ke endpoint.url Anda)Rahasia webhook tingkat organisasi (sama dengan webhook URL tunggal lama) — bukan rahasia per endpoint

Simpan rahasia di pengelola rahasia atau variabel environment Anda — jangan pernah melakukan commit.

Implementasi referensi

Keempatnya memverifikasi body permintaan mentah:

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 "")
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
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
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

Integrasi khusus framework

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}
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);
  },
);
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})

Memverifikasi panggilan alat

Saat agen memanggil salah satu alat fungsi Anda secara langsung (alat tersebut memiliki endpoint), permintaan membawa dua header ThunderPhone bersama endpoint.headers yang Anda konfigurasi:

  • X-ThunderPhone-Call-ID — ID numerik panggilan aktif.
  • X-ThunderPhone-Signature — HMAC-SHA256, menggunakan rahasia webhook tingkat organisasi Anda sebagai kunci, atas byte isi permintaan yang persis sama.

Helper verify() yang sama dapat digunakan tanpa perubahan, dengan dua hal berikut:

  1. Alat GET / DELETE tidak memiliki isi. Argumen dikirim sebagai parameter kueri, dan tanda tangan dihitung atas string byte kosong — jadi verify(b"", sig, secret) (Python) atau verify(Buffer.alloc(0), sig, secret) (Node). Jangan hash string kueri.
  2. Organisasi tanpa webhook lama yang dikonfigurasi tidak memiliki rahasia organisasi. Dalam kasus tersebut, panggilan alat hanya membawa X-ThunderPhone-Call-ID dan tidak ada header tanda tangan. Konfigurasikan webhook lama (PUT /v1/webhook) untuk mendapatkan rahasia penandatanganan, atau autentikasi panggilan alat dengan header Anda sendiri melalui endpoint.headers.
@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 alat dalam mode webhook (alat tanpa endpoint, dikirimkan ke webhook organisasi Anda sebagai telephony.tool / web.tool) adalah webhook bertanda tangan biasa — resep standar di atas berlaku. Lihat Alat Fungsi untuk kedua bentuk permintaan.

Kesalahan umum

Serialisasi ulang dengan pemformatan default

Mengurai body lalu membuangnya kembali dengan pengaturan default library JSON Anda (spasi setelah , / :, kunci berdasarkan urutan 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.

Framework mengurai JSON secara otomatis

Middleware express.json() Express mengonsumsi stream body sehingga Anda kehilangan byte mentah. Gunakan express.raw() khusus pada rute webhook, atau buffer body mentah dalam middleware sebelumnya. Hal yang sama berlaku untuk NestJS / Koa — periksa dokumentasi "raw body" mereka.

Perbandingan yang tidak aman terhadap waktu

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.

Rahasia yang salah untuk endpoint tool

Panggilan langsung ke endpoint tool ditandatangani dengan rahasia webhook tingkat organisasi (GET /v1/webhook) — bukan dengan rahasia per-endpoint dari /v1/developer/webhook-endpoints. Gunakan kembali fungsi verify() yang sama, tetapi pastikan Anda memberinya rahasia organisasi pada rute tool.

Meng-hash string kueri pada tool GET/DELETE

Untuk metode tool tanpa body, tanda tangan mencakup string byte kosong, sehingga hanya diperlukan satu resep universal: HMAC body permintaan mentah, apa pun isinya. Meng-hash URL atau string kueri tidak akan pernah cocok.

Tidak mengembalikan 401 saat tidak cocok

Mengembalikan 200 saat verifikasi gagal menjadikan handler sebagai target replay. Selalu respons dengan non-2xx jika verifikasi gagal.


Langkah berikutnya