> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payven.com.tr/llms.txt
> Use this file to discover all available pages before exploring further.

# İmza Doğrulama

> Gelen webhook isteklerinin Payven'den geldiğini HMAC-SHA256 ile doğrulayın.

Payven her webhook isteğinde **HMAC-SHA256 imza** gönderir. Bu imza, isteğin gerçekten Payven'den geldiğini ve request body'sinin değiştirilmediğini garanti eder. Public webhook endpoint'inizi açıyorsanız imza doğrulama **zorunludur**.

## İmza algoritması

Payven her istekte iki header gönderir:

```http theme={null}
X-Payven-Timestamp: 1714742400
X-Payven-Signature: sha256=4f1d8c92ab7e3bcf9a2e1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f
```

İmza şu formülle üretilir:

```
imzalanacak_string = "<X-Payven-Timestamp>" + "." + "<request_body_raw_string'i>"
hmac               = HMAC_SHA256(subscription.secret, imzalanacak_string)
beklenen_imza      = "sha256=" + hex(hmac).lower()
```

Sizin tarafınızda bu üç adımı uygulayıp `X-Payven-Signature` header'ı ile karşılaştırırsınız:

1. Timestamp'in **5 dakika içinde** olduğunu kontrol edin (replay koruması)
2. Body'i **string olarak** (parse etmeden) HMAC-SHA256 ile imzalayın
3. Sonucu sabit zamanlı (constant-time) karşılaştırma ile doğrulayın

<Warning>
  **Body'i parse etmeden imzalayın.** JSON.parse() + JSON.stringify() döngüsü
  property sırasını değiştirip imzayı bozar. Raw string'i mutlaka middleware'den
  önce okuyun.
</Warning>

## Örnek implementasyonlar

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import crypto from "crypto";
  import express from "express";

  const app = express();

  // IMPORTANT: imza için ham body gerekir, JSON parse edilmeden saklayın
  app.use("/webhooks/payven", express.raw({ type: "application/json" }));

  app.post("/webhooks/payven", (req, res) => {
    const signature = req.header("x-payven-signature");
    const timestamp = req.header("x-payven-timestamp");
    const body = req.body.toString("utf8");

    // 1. Timestamp 5 dakika içinde mi?
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
      return res.status(401).send("Timestamp out of tolerance");
    }

    // 2. İmzayı hesapla
    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", process.env.PAYVEN_WEBHOOK_SECRET)
        .update(`${timestamp}.${body}`)
        .digest("hex");

    // 3. Sabit zamanlı karşılaştır
    const sigBuf = Buffer.from(signature ?? "", "utf8");
    const expBuf = Buffer.from(expected, "utf8");
    if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(body);
    console.log("Verified event:", event.type, event.id);
    res.status(200).end();
  });
  ```

  ```python Python (FastAPI) theme={null}
  import hmac, hashlib, time
  from fastapi import FastAPI, Request, HTTPException

  app = FastAPI()
  PAYVEN_WEBHOOK_SECRET = "whsec_..."

  @app.post("/webhooks/payven")
  async def webhook(request: Request):
      body = await request.body()
      signature = request.headers.get("x-payven-signature", "")
      timestamp = request.headers.get("x-payven-timestamp", "0")

      # 1. Timestamp 5 dakika içinde mi?
      if abs(int(time.time()) - int(timestamp)) > 300:
          raise HTTPException(401, "Timestamp out of tolerance")

      # 2. İmzayı hesapla
      payload = f"{timestamp}.{body.decode('utf-8')}".encode("utf-8")
      expected = "sha256=" + hmac.new(
          PAYVEN_WEBHOOK_SECRET.encode("utf-8"),
          payload,
          hashlib.sha256,
      ).hexdigest()

      # 3. Sabit zamanlı karşılaştır
      if not hmac.compare_digest(signature, expected):
          raise HTTPException(401, "Invalid signature")

      event = await request.json()
      return {"received": event["id"]}
  ```

  ```csharp C# (ASP.NET Core) theme={null}
  using System.Security.Cryptography;
  using System.Text;
  using Microsoft.AspNetCore.Mvc;

  [ApiController]
  [Route("webhooks/payven")]
  public class WebhookController : ControllerBase
  {
      private const string Secret = "whsec_...";

      [HttpPost]
      public async Task<IActionResult> Receive()
      {
          var signature = Request.Headers["X-Payven-Signature"].ToString();
          var timestamp = Request.Headers["X-Payven-Timestamp"].ToString();

          // 1. Timestamp 5 dakika içinde mi?
          if (!long.TryParse(timestamp, out var ts)
              || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300)
          {
              return Unauthorized("Timestamp out of tolerance");
          }

          // 2. Body'i string olarak oku
          Request.EnableBuffering();
          using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
          var body = await reader.ReadToEndAsync();
          Request.Body.Position = 0;

          // 3. İmzayı hesapla
          var payload = $"{timestamp}.{body}";
          using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
          var hashHex = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)))
              .ToLowerInvariant();
          var expected = $"sha256={hashHex}";

          // 4. Sabit zamanlı karşılaştır
          if (!CryptographicOperations.FixedTimeEquals(
                  Encoding.UTF8.GetBytes(signature),
                  Encoding.UTF8.GetBytes(expected)))
          {
              return Unauthorized("Invalid signature");
          }

          // İşle...
          return Ok();
      }
  }
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "io"
      "net/http"
      "strconv"
      "time"
  )

  const secret = "whsec_..."

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
      signature := r.Header.Get("X-Payven-Signature")
      timestamp := r.Header.Get("X-Payven-Timestamp")

      // 1. Timestamp 5 dakika içinde mi?
      ts, err := strconv.ParseInt(timestamp, 10, 64)
      if err != nil || abs(time.Now().Unix()-ts) > 300 {
          http.Error(w, "Timestamp out of tolerance", http.StatusUnauthorized)
          return
      }

      // 2. Body'i oku
      body, _ := io.ReadAll(r.Body)

      // 3. İmzayı hesapla
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write([]byte(timestamp + "." + string(body)))
      expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

      // 4. Sabit zamanlı karşılaştır
      if !hmac.Equal([]byte(signature), []byte(expected)) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      w.WriteHeader(http.StatusOK)
  }

  func abs(x int64) int64 { if x < 0 { return -x }; return x }
  ```

  ```php PHP theme={null}
  <?php
  $secret = getenv("PAYVEN_WEBHOOK_SECRET");

  $signature = $_SERVER["HTTP_X_PAYVEN_SIGNATURE"] ?? "";
  $timestamp = $_SERVER["HTTP_X_PAYVEN_TIMESTAMP"] ?? "0";
  $body      = file_get_contents("php://input");

  // 1. Timestamp 5 dakika içinde mi?
  if (abs(time() - intval($timestamp)) > 300) {
      http_response_code(401);
      exit("Timestamp out of tolerance");
  }

  // 2. İmzayı hesapla
  $payload  = $timestamp . "." . $body;
  $expected = "sha256=" . hash_hmac("sha256", $payload, $secret);

  // 3. Sabit zamanlı karşılaştır
  if (!hash_equals($expected, $signature)) {
      http_response_code(401);
      exit("Invalid signature");
  }

  $event = json_decode($body, true);
  http_response_code(200);
  ```
</CodeGroup>

## Önemli detaylar

<Check>**Body'i raw olarak okuyun.** Express'te `express.raw({ type: "application/json" })`, ASP.NET'te `Request.EnableBuffering()` + StreamReader. Framework'ün otomatik JSON parser'ını **bypass edin** veya parse'tan önce raw bytes'ı kaydedin.</Check>
<Check>**Timestamp toleransı 5 dakika** (`±300 saniye`). Daha gevşek tolerance replay attack riski yaratır.</Check>
<Check>**Sabit zamanlı karşılaştırma** kullanın (`crypto.timingSafeEqual`, `hmac.compare_digest`, `CryptographicOperations.FixedTimeEquals`, `hash_equals`). `==` ile karşılaştırma timing attack açığı yaratır.</Check>
<Check>**Secret'ı environment variable** olarak saklayın, public repo'ya commit etmeyin.</Check>
<Check>**Ham UTF-8 byte'larıyla imzalayın.** Pretty-print, BOM, satır sonu farklılıkları imzayı kıracaktır.</Check>

## Secret rotasyonu

Bir webhook subscription'ın secret'ını rotasyonu için:

```bash theme={null}
curl -X POST https://vpos.payven.com.tr/api/v1/webhook-subscriptions/8e3f5c12-.../rotate-secret \
  -H "Authorization: Bearer $PAYVEN_TOKEN"
```

Yanıt yeni secret'ı döner. **Eski secret 24 saat boyunca geçerli kalır** — bu sürede her iki secret ile imzalanmış istekler kabul edilir, böylece zero-downtime geçiş yaparsınız:

```javascript theme={null}
// Geçiş döneminde her iki secret'ı dene
const valid =
  verify(body, signature, oldSecret) ||
  verify(body, signature, newSecret);
```

## İmza bozuk geliyorsa

| Sorun                              | Çözüm                                                                                            |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
| Body parse + re-stringify ediliyor | `app.use(express.raw())` veya raw body capture middleware ekleyin                                |
| Timestamp drift > 5 dakika         | Sunucu saatinizi NTP ile senkronize edin                                                         |
| Boşluk / satır sonu fark           | Body'i `Buffer` veya `bytes` olarak saklayın, `string` dönüşümlerinde encoding belirtin (`utf8`) |
| Secret yanlış                      | Konsoldan `whsec_` ile başlayan değeri tam olarak kopyaladığınızdan emin olun                    |
| `X-Payven-Signature` boş           | Reverse proxy header strip yapıyor olabilir — Nginx config'de `proxy_pass_request_headers on`    |

Hâlâ çözemediğiniz durumda `X-Payven-Delivery-Id` ile birlikte [destek ekibimize](/resources/support) yazın.
