ThunderPhone 2.0 yayında.Kendi başınıza kullanmaya başlayın; dakikada 2¢'den başlayan fiyatlarla.Duyuruyu okuyun

Operations

Webhook imzalarını doğrulayın

ThunderPhone

Sunucunuza gönderdiğimiz her istek — webhook teslimatları ve araç uç noktası çağrıları — X-ThunderPhone-Signature üst bilgisinde bir HMAC-SHA256 imzası taşır. Doğrulamayı bir kez doğru şekilde uygulayın ve aynı yardımcı işlevini her işleyiciye ekleyin.

Algoritma

  1. Ham istek gövdesini okuyun — size POST ettiğimiz tam baytları.
  2. hmac_sha256(secret, body).hexdigest() hesaplayın.
  3. X-ThunderPhone-Signature ile sabit zamanda karşılaştırın. (Basit dize karşılaştırması zamanlama bilgilerini sızdırır.)

İlettiğimiz tam baytları imzalarız; bu nedenle ham gövdeyi doğrulamak her zaman çalışır. Bu baytlar aynı zamanda yükün standart JSON serileştirmesidir — anahtarlar alfabetik olarak sıralanır, ayırıcılar sıkışıktır (boşluksuz , ve :), kodlama UTF-8'dir. Çerçeveniz yalnızca ayrıştırılmış JSON'u sunuyorsa, size tamamen eşdeğer ikinci bir yöntem sağlar: standart biçimde yeniden serileştirin ve bunun HMAC'ini hesaplayın.

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

Ham gövdeyi tercih edin — bir adım daha azdır ve bazı dillerdeki JSON sayı gidiş-dönüş dönüştürme farklılıklarından etkilenmez.

Hangi gizli anahtar?

KaynakGizli anahtar
Webhook uç noktası (/v1/developer/webhook-endpoints)Oluşturulurken bir kez döndürülen, uç nokta başına secret (48 hex karakter)
Eski tek URL'li webhookGET /v1/webhook çağrısında döndürülen, kuruluş başına secret
Araç uç noktası çağrısı (endpoint.url adresinize doğrudan çağrı)Kuruluş düzeyindeki webhook gizli anahtarı (eski tek URL'li webhook ile aynı anahtar) — uç nokta başına bir gizli anahtar değildir

Gizli anahtarı gizli anahtar yöneticinizde veya ortam değişkeninde saklayın — asla sürüm kontrolüne eklemeyin.

Referans uygulamalar

Dördü de ham istek gövdesini doğrular:

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

Çerçeveye özgü bağlantı

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

Araç çağrılarını doğrulama

Ajan, işlev araçlarınızdan birini doğrudan çağırdığında (aracın bir endpoint değeri vardır), istek yapılandırdığınız endpoint.headers ile birlikte iki ThunderPhone üst bilgisi taşır:

  • X-ThunderPhone-Call-ID — canlı çağrının sayısal kimliği.
  • X-ThunderPhone-Signature — tam istek gövdesi baytları üzerinden, kuruluş düzeyindeki webhook gizli anahtarınız ile anahtarlanmış HMAC-SHA256.

Aynı verify() yardımcısı, iki farkla değiştirilmeden çalışır:

  1. GET / DELETE araçlarının gövdesi yoktur. Bağımsız değişkenler sorgu parametreleri olarak iletilir ve imza boş bayt dizisi üzerinden hesaplanır — yani verify(b"", sig, secret) (Python) veya verify(Buffer.alloc(0), sig, secret) (Node). Sorgu dizesini karma değerine dönüştürmeyin.
  2. Eski webhook yapılandırılmamış kuruluşların kuruluş gizli anahtarı yoktur. Bu durumda araç çağrıları yalnızca X-ThunderPhone-Call-ID taşır ve imza üst bilgisi içermez. İmzalama gizli anahtarı almak için eski webhook'u (PUT /v1/webhook) yapılandırın veya araç çağrılarını endpoint.headers aracılığıyla kendi üst bilginizle doğrulayın.
@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)
    ...

Webhook modunda araç yönlendirmesi (endpoint içermeyen ve kuruluş webhook'unuza telephony.tool / web.tool olarak iletilen araçlar) normal bir imzalı webhook'tur — yukarıdaki standart yöntem geçerlidir. Her iki istek biçimi için İşlev Araçları sayfasına bakın.

Yaygın hatalar

Varsayılan biçimlendirmeyle yeniden serileştirme

Gövdeyi ayrıştırıp JSON kitaplığınızın varsayılanlarıyla yeniden dökmek (, / : sonrasında boşluklar, ekleme sırasına göre anahtarlar) farklı baytlar üretir ve HMAC'i bozar. Ham gövdeyi doğrulayın — veya yeniden serileştirmeniz gerekiyorsa kanonik biçimimizle tam olarak eşleştirin: sıralanmış anahtarlar, sıkıştırılmış ayırıcılar, UTF-8.

Çerçeve JSON'u otomatik ayrıştırıyor

Express'in express.json() ara yazılımı gövde akışını tüketir ve ham baytları kaybedersiniz. Webhook rotasında özellikle express.raw() kullanın veya ham gövdeyi bir ön ara yazılımda arabelleğe alın. NestJS / Koa için de aynı durum geçerlidir — "ham gövde" belgelerini kontrol edin.

Zamanlama açısından güvenli olmayan karşılaştırma

JS'de expected === signature veya Python'da expected == signature zamanlamaya göre değişen karşılaştırmalardır. Sırasıyla crypto.timingSafeEqual veya hmac.compare_digest kullanın. Performans farkı yok denecek kadar azdır.

Araç uç noktaları için yanlış gizli anahtar

Doğrudan araç uç noktası çağrıları, kuruluş düzeyi webhook gizli anahtarı (GET /v1/webhook) ile imzalanır — /v1/developer/webhook-endpoints içindeki uç noktaya özel gizli anahtarlarla değil. Aynı verify() işlevini yeniden kullanın, ancak araç rotalarında kuruluş gizli anahtarını verdiğinizden emin olun.

GET/DELETE araçlarında sorgu dizesini karma değerine dönüştürme

Gövdesiz araç yöntemlerinde imza boş bayt dizesini kapsar ve tek bir evrensel yöntem korunur: Ne olursa olsun ham istek gövdesine HMAC uygulayın. URL'yi veya sorgu dizesini karma değerine dönüştürmek asla eşleşmez.

Eşleşmezse 401 döndürmemek

Doğrulama başarısız olduğunda 200 döndürmek, işleyiciyi yeniden oynatma saldırılarının hedefi hâline getirir. Doğrulama başarısız olursa her zaman 2xx olmayan bir yanıt verin.


Sonraki adımlar