---
title: "Webhook imzalarını doğrulama"
description: "ThunderPhone tarafından gönderilen her webhook ve araç isteği bir zaman damgasıyla imzalanır. Buradaki yöntem veya SDK yardımcısıyla doğrulayın ve beş dakikadan eski tekrar oynatılmış istekleri reddedin."
---

Sunucunuza gönderdiğimiz her istek (webhook teslimleri, eski
tek URL'li webhook ve araç uç noktası çağrıları) iki imza
başlığı içerir. Her ikisi de aynı gizli anahtarla HMAC-SHA256 kullanır.

| Başlık | İmzaladığı veriler | Yeniden oynatma koruması |
|--------|-------|-------------------|
| `X-ThunderPhone-Signature-V2` (önerilen) | Zaman damgası, yöntem, yol, sorgu ve gövde | 5 dakikadan eski istekleri reddedin |
| `X-ThunderPhone-Signature` | Yalnızca gövde | Yok: ele geçirilen bir istek her zaman doğrulanır |

Yeni entegrasyonlarda `X-ThunderPhone-Signature-V2` doğrulamasını yapın ve
mevcut entegrasyonları buna taşıyın. `X-ThunderPhone-Signature` her
istekte değişmeden gönderilmeye devam eder; bu nedenle buna dayalı doğrulayıcılar
çalışmaya devam eder. Göndermeyi durdurmadan önce bir tarih duyuracağız.

## İmza V2

```http
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
```

- `t`, isteği gönderdiğimiz andaki Unix zamanıdır ve saniye cinsindendir. Yeniden denenen
  bir teslimat, gönderildiğinde kendi `t` değeriyle tekrar imzalanır.
- `v1`, aşağıdaki imzalı dizenin, gizli anahtarınız kullanılarak oluşturulmuş onaltılık HMAC-SHA256 değeridir.

Değer, virgülle ayrılmış `key=value` çiftlerinin listesidir. Tanımadığınız anahtarları
yok sayın ve herhangi bir `v1` eşleşirse isteği kabul edin: gelecekteki bir değişiklik sırasında
ikinci bir imza ekleyebiliriz.

### İmzalı dize

Her biri sonuncu hariç bir yeni satırla (`\n`, bayt `0x0A`) biten dört alan:

```text
<t>\n<METHOD>\n<path and query>\n<body>
```

| Alan | Değer |
|-------|-------|
| `t` | Başlıktaki `t` değeri |
| `METHOD` | Büyük harfli HTTP yöntemi: `POST`, `GET`, `PUT`, `PATCH` veya `DELETE` |
| path and query | HTTP istek satırındaki istek hedefi: yol, ardından varsa `?` ve sorgu dizesi. Şema, ana makine veya parça içermez. Kodunu çözmeden ya da parametreleri yeniden sıralamadan, geldiği şekliyle kullanın |
| `body` | Ham istek gövdesi. `GET` ve `DELETE` araç çağrılarının gövdesi yoktur; bu nedenle imzalı dizeleri yolun ardından gelen yeni satırla biter |

Doğrulamak için:

1. Herhangi bir ayrıştırmadan önce başlığı, yöntemi, ham yolu ve
   sorguyu ve ham gövdeyi okuyun.
2. Başlıktan `t` ve tüm `v1` değerlerini alın.
3. `t` değeri saatinizden **300 saniyeden** fazla önce veya sonra ise
   isteği reddedin.
4. Gizli anahtarınızla imzalı dizenin onaltılık HMAC-SHA256 değerini hesaplayın.
5. Sabit zamanda `v1` ile karşılaştırın. Hiçbiri eşleşmezse `401` yanıtını verin.

### Uygulamalı örnek

Gizli anahtar `whsec_example_4c1f2a9b`. Bu isteği
`t = 1767225600` (2026-01-01 00:00:00 UTC) anında gönderiyoruz:

```http
POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e

{"customer_id":"cust_123"}
```

İmzalı dize, gövdeden sonra yeni satır olmadan şu dört satırdır:

```text
1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}
```

HMAC-SHA256 değeri
`03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e`.
Uygulamanızı buna göre kontrol edin veya `openssl` ile yeniden oluşturun:

```bash
printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
  | openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'
```

Aynı saniyede `/tools/lookup?customer_id=cust_123` adresine yapılan bir `GET`
araç çağrısının gövdesi boştur. İmzalı dizgesi
`1767225600\nGET\n/tools/lookup?customer_id=cust_123\n`, imzası ise
`3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280` olur.

### SDK'lerle

[Python ve TypeScript SDK'leri](/api-reference/sdks), bir yardımcı içerir
(`thunderphone` 0.2.0 veya üzeri, `@thunderphone/sdk` 0.2.0 veya üzeri). Bu yardımcı
5 dakikalık pencereyi denetler, sabit zamanda karşılaştırır ve kesinti olmadan
anahtar rotasyonu yapabilmeniz için bir gizli anahtar listesini kabul eder.

<CodeGroup>
```python Python
from thunderphone import verify_signature

ok = verify_signature(
    header,            # the X-ThunderPhone-Signature-V2 value
    secret=SECRET,     # or [NEW_SECRET, OLD_SECRET] while rotating
    method=method,     # "POST"
    path=path,         # "/tools/lookup?region=us", exactly as received
    body=raw_body,     # bytes, before any JSON parsing
    tolerance=300,     # optional; seconds
)
```

```javascript Node.js
import { verifySignature } from "@thunderphone/sdk";

const ok = verifySignature({
  header,               // the X-ThunderPhone-Signature-V2 value
  secret: SECRET,       // or [NEW_SECRET, OLD_SECRET] while rotating
  method,               // "POST"
  path,                 // "/tools/lookup?region=us", exactly as received
  body: rawBody,        // Buffer or string, before any JSON parsing
  toleranceSeconds: 300, // optional
});
```
</CodeGroup>

### SDK olmadan

<CodeGroup>
```python Python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
    """path is the path plus ?query exactly as received; body is the raw body."""
    timestamp, signatures = None, []
    for item in (header or "").split(","):
        key, _, value = item.strip().partition("=")
        if key == "t" and value.isdigit() and len(value) <= 15:
            timestamp = int(value)
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
```

```javascript Node.js
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
  let timestamp;
  const signatures = [];
  for (const item of (header || "").split(",")) {
    const [key, value = ""] = item.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
      .update(body)
      .digest("hex"),
  );
  return signatures.some((sig) => {
    const candidate = Buffer.from(sig);
    return (
      candidate.length === expected.length &&
      crypto.timingSafeEqual(candidate, expected)
    );
  });
}
```
</CodeGroup>

### Çerçeve bağlantısı

Her örnek ham yolu ve sorguyu, ardından ham gövdeyi okur ve SDK yardımcısını çağırır.

<CodeGroup>
```python FastAPI
import json

from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature

app = FastAPI()


def raw_path_and_query(request: Request) -> str:
    path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
    query = request.scope.get("query_string", b"").decode("latin-1")
    return f"{path}?{query}" if query else path


@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()  # raw bytes, NOT request.json()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
```

```javascript Express
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

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 ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl, // path and query as received
      body: req.body,
    });
    if (!ok) 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 HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature


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

`req.originalUrl` (Express) ile ASGI `raw_path` ve `query_string`
(FastAPI), hedefi alındığı biçimiyle tam olarak içerir. Django'nun
`get_full_path()` yöntemi ve Flask'ın `request.path` değeri önce yolu çözer;
bu, normal yollar için aynı dizeyi verir. Uç nokta yolunuz `%2F`, `;` veya `=`
gibi yüzde kodlu ya da ayrılmış karakterler içeriyorsa, bunun yerine sunucunuzun
sağladığı ham hedefi okuyun (örneğin Gunicorn'da `request.META["RAW_URI"]`).

## Hangi gizli anahtar?

Her iki başlık da aynı gizli anahtarla imzalanır.

| Kaynak | Gizli anahtar |
|--------|--------|
| [Webhook uç noktası](/tr/webhooks/endpoints) (`/v1/developer/webhook-endpoints`) | Oluşturulurken bir kez döndürülen, uç nokta başına `secret` (48 hex karakter) |
| [Eski tek URL'li webhook](/api-reference/organizations#legacy-single-url-webhook) | `GET /v1/webhook` çağrısında döndürülen, kuruluş başına `secret` |
| [Araç uç noktası çağrısı](/tr/tools/overview) (`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.

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

Yapay zeka ajanı [fonksiyon araçlarınızdan](/tr/tools/overview) birini doğrudan çağırdığında (aracın bir `endpoint` değeri vardır), istek yapılandırdığınız `endpoint.headers` ile birlikte şu ThunderPhone başlıklarını içerir:

- `X-ThunderPhone-Call-ID` — canlı çağrının sayısal kimliği.
- `X-ThunderPhone-Signature-V2` — kuruluş düzeyindeki **webhook gizli anahtarınızla** oluşturulan, yukarıdaki zaman damgalı imza.
- `X-ThunderPhone-Signature` — aynı anahtarı kullanan, yalnızca gövdeye yönelik eski imza.

Webhook'lara kıyasla iki fark vardır:

1. **`GET` / `DELETE` araçlarının gövdesi yoktur.** Bağımsız değişkenler sorgu parametreleri olarak iletilir. V2 imzası bunları yol ve sorgu üzerinden kapsar; gövde alanı boştur. Ham sorgu dizesiyle doğrulayın: parametreleri ayrıştırıp yeni bir sorgu dizesi oluşturmak, sıralarını veya kodlamalarını değiştirebilir. Eski başlık boş bayt dizesini imzalar.
2. **Eski bir webhook yapılandırılmamış kuruluşların kuruluş gizli anahtarı yoktur.** Bu durumda araç çağrıları yalnızca `X-ThunderPhone-Call-ID` içerir ve imza başlıkları 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 başlığınızla kimlik doğrulayın.

```python
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=ORG_WEBHOOK_SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    args = json.loads(body) if body else dict(request.query_params)
    ...
```

Express'te bir gövde ayrıştırıcısı, `GET` isteğinde `req.body` değerini boş nesne olarak bırakır; bu nedenle `Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0)` iletin.

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ı](/tr/tools/overview) sayfasına bakın.

## Eski imza: `X-ThunderPhone-Signature`

Orijinal başlık, yalnızca ham gövdenin aynı gizli anahtarla hesaplanan onaltılık HMAC-SHA256 değeridir. Zaman damgası içermez; bu nedenle imzalı bir isteği ele geçiren herkes isteği yeniden iletebilir. Entegrasyonlar V2'ye geçerken her istekte gönderilmeye devam eder.

1. **Ham** istek gövdesini — size gönderdiğimiz tam baytları — okuyun.
2. `hmac_sha256(secret, body).hexdigest()` hesaplayın.
3. `X-ThunderPhone-Signature` ile **sabit zamanda** karşılaştırın.

İlettiğimiz baytları tam olarak imzalarız. Bu baytlar aynı zamanda yükün **kanonik 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, kanonik olarak yeniden serileştirin ve bunun HMAC'ini hesaplayın:

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

Ham gövdeyi tercih edin — bu, bir adım daha az gerektirir ve bazı dillerde JSON sayılarının ileri geri dönüştürülmesindeki tuhaflıklara karşı dayanıklıdır. Aynı durum V2'nin gövde alanı için de geçerlidir.

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

## Yaygın hatalar

<AccordionGroup>
  <Accordion title="Saat sapması">
    5 dakikalık pencere, sunucunuzun saatine göre ölçülür. Saati NTP ile
    eşzamanlı tutun; 5 dakikadan fazla sapmış bir saat her isteği
    reddeder.
  </Accordion>

  <Accordion title="Bir proxy yolu yeniden yazar">
    İmzalanan yol, talep ettiğimiz yoldur: ThunderPhone'da
    yapılandırdığınız URL'nin yolu. Ters proxy veya API ağ geçidi, uygulamanız
    isteği görmeden önce bir öneki kaldırır ya da eklerse doğrulamadan önce
    gönderdiğimiz yolu yeniden oluşturun (örneğin, öneki geri ekleyin).
  </Accordion>

  <Accordion title="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ıralı 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.
  </Accordion>

  <Accordion title="Ç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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Araç uç noktaları için yanlış gizli anahtar">
    Doğrudan araç uç noktası çağrıları, **kuruluş düzeyindeki webhook
    gizli anahtarı** (`GET /v1/webhook`) ile imzalanır — 
    `/v1/developer/webhook-endpoints` içindeki uç noktaya özgü herhangi bir gizli anahtarla değil. Aynı yardımcıyı
    yeniden kullanın, ancak araç yollarında ona kuruluş gizli anahtarını
    verdiğinizden emin olun.
  </Accordion>

  <Accordion title="GET/DELETE araçlarındaki sorgu dizeleri">
    V2, sorgu dizesini gönderildiği şekliyle imzalar. Eski başlık,
    boş gövdeyi imzalar ve sorgu dizesini hiçbir zaman imzalamaz; bu nedenle
    eski başlık için URL'yi özetlemek hiçbir zaman eşleşmez.
  </Accordion>

  <Accordion title="Pencere içindeki yinelenenler">
    Zaman damgası, pencere içindeki ikinci bir kopyayı değil, 5 dakikadan eski
    yeniden oynatmaları engeller. Uç nokta teslimatları en az bir kez gerçekleşir; bu nedenle
    aynı olayın yeniden denemesi de gelebilir; zarfın `event_id` değerine göre
    tekilleştirin.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

---

## Sonraki adımlar

<CardGroup cols={2}>
  <Card title="Webhook genel bakışı" icon="bolt" href="/tr/webhooks/overview">
    Teslimat semantiği, yeniden denemeler, kaynak IP'leri.
  </Card>
  <Card title="Webhook uç noktaları" icon="plug" href="/tr/webhooks/endpoints">
    Birden çok URL'yi yönetin, gizli anahtarları döndürün.
  </Card>
  <Card title="İşlev Araçları" icon="screwdriver-wrench" href="/tr/tools/overview">
    İki araç çağırma yolu ve istek biçimleri.
  </Card>
  <Card title="Araç entegrasyonları" icon="wrench" href="/tr/guides/build-tool-integration">
    Araç destekli eksiksiz bir entegrasyonu uçtan uca oluşturun.
  </Card>
</CardGroup>
