# Webhook imzası doğrulama

Ödeme bildirimi, yalnızca mağazanızın ve hizmetimizin bildiği bir adrese gelir. Ancak adres sızabilir — günlüklerden, yapılandırmadan, geliştirme geçmişinden. Gerçek bildirimi sahtesinden ayırmak için her istek, webhook gizli anahtarıyla imzalanır.

Siparişi teslim etmeden veya durumunu değiştirmeden önce imzayı doğrulayın.

## İmza başlıkları

| Başlık | Değer |
| --- | --- |
| `X-Timestamp` | Gönderim zamanı, saniye cinsinden unix time |
| `X-Signature` | `sha256=` öneki ve hex olarak HMAC-SHA256 |

İmzalanan dize, gönderim zamanı, bir nokta ve istek gövdesinden oluşur:

```
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
```

İmzalama anahtarı, proje ayarlarındaki Webhook gizli anahtarı (Webhook secret) alanıdır. Değer ele geçirilirse orada yeniden verebilirsiniz.

## Doğrulama adımları

1. Ham istek gövdesini, JSON'u ayrıştırmadan önce alın.
2. `X-Timestamp` değerini, bir noktayı ve gövdeyi birleştirin.
3. Webhook gizli anahtarıyla HMAC-SHA256'yı hesaplayın.
4. Sonucu, sabit zamanlı karşılaştırma kullanarak `X-Signature` ile karşılaştırın.
5. `X-Timestamp` şu anki zamandan çok sapıyorsa isteği reddedin.

## Uygulama örnekleri

**PHP**

```php showLineNumbers
<?php
$secret = 'webhook secret from your project settings';

$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);
```

**TypeScript**

```typescript showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { type Request, type Response } from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = (req.body as Buffer).toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**JavaScript**

Aynı işleyicinin türsüz hâli — düz bir JavaScript projesi için.

```js showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = req.body.toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**Python**

```python showLineNumbers
import hmac
import hashlib
import time

from flask import Flask, request

app = Flask(__name__)
secret = 'webhook secret from your project settings'

@app.post('/webhook')
def webhook():
    timestamp = request.headers.get('X-Timestamp', '')
    signature = request.headers.get('X-Signature', '')
    body = request.get_data(as_text=True)

    if abs(time.time() - int(timestamp or 0)) > 300:
        return '', 400

    expected = 'sha256=' + hmac.new(
        secret.encode(),
        f'{timestamp}.{body}'.encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return '', 403

    data = request.get_json()

    # Handle the order

    return '', 200
```

## Akılda tutulması gerekenler

**Ham gövde imzalanır.** JSON'u ayrıştırıp yeniden oluşturursanız anahtar sırası ve boşluklar değişir — imza eşleşmez. HMAC'i ayrıştırmadan önce hesaplayın.

**Her yeniden denemenin kendi imzası vardır.** Gönderim zamanı her yeniden denemede yenidir, dolayısıyla imza da farklıdır. Gelecekteki isteklerle karşılaştırmak için imza saklamayın.

**Gizli anahtar yeniden verilebilir.** Eski gizli anahtar, yeniden verildikten hemen sonra çalışmayı durdurur; bu yüzden mağazanızdaki değeri aynı anda güncelleyin.

**İmza doğrulama, idempotentliğin yerini tutmaz.** İmza, isteğin gerçek olduğunu kanıtlar; onu ilk kez gördüğünüzü değil. Yeniden deneme, geçerli bir imzayla gelir — `invoice.id` üzerinden kontrol edin.
