# Webhook kurmak ve doğrulamak

> **Ne yaparsınız:** Değişikliklerin HTTPS alıcınıza imzalı olarak gönderilmesini sağlar, imzayı doğrular ve teslimleri güvenle işlersiniz.
> **Hangi uçlar:** [Webhook adresi oluştur](https://developer.siparis.app/uc/webhook-adresi-olustur), [Webhook denemesi gönder](https://developer.siparis.app/uc/webhook-denemesi-gonder), [Webhook teslimlerini listele](https://developer.siparis.app/uc/webhook-teslimlerini-listele), [Webhook sırrını yenile](https://developer.siparis.app/uc/webhook-sirrini-yenile), [Olayı yeniden gönder](https://developer.siparis.app/uc/olayi-yeniden-gonder), [Webhook adresini kapat](https://developer.siparis.app/uc/webhook-adresini-kapat)
> **Gereken izin:** `webhooks:manage` ve seçtiğiniz olayların okuma kapsamları (ör. `order.created` için `orders:read`).

Yönetim API’sinin temel adresi `https://api.siparis.app/v1`. Dış API yalnız kendi bağlantısının (panelde anahtarı ve izinlerini tutan kayıt) webhook adreslerini yönetir.

## Adresi ekleyin ve denemeyi tamamlayın

Panelde Geliştirici › Webhook adresleri bölümüne genel internete açık HTTPS/443 adresi ekleyin veya [Webhook adresi oluştur](https://developer.siparis.app/uc/webhook-adresi-olustur) kullanın. Adreste kullanıcı bilgisi veya sır olmamalıdır. `webhook.test` seçilmesi gerekmeden otomatik gönderilir ve `GET /events` akışında yer almaz. Kart reddi için `payment.failed`, azalan stok için `inventory.low`; abonelikler için `subscription.*`, kuponlar için `coupon.*`, kanal siparişi ve bağlantı sorunu için `channel.*` seçebilirsiniz; yeni türler eski joker aboneliklere otomatik eklenmez. Yeni adres `pending_test` durumunda açılır, imzalı `webhook.test` kuyruğa alınır. Alıcınız doğrulayıp 2xx vermeden gerçek olaylar başlamaz.

Oluşturma yanıtındaki `secret` (`whsec_…`) yalnız bir kez gösterilir; sunucunuzun gizli ortam değişkenine kaydedin. Aynı `Idempotency-Key` (tekrar anahtarı: aynı isteği güvenle yeniden göndermek için ürettiğiniz metin) ile tekrarda güncel adres bilgisi döner, `secret` alanı dönmez; bu kural sır döndürmede de geçerlidir. API anahtarıyla webhook sırrı farklıdır. Sonucu [Webhook teslimlerini listele](https://developer.siparis.app/uc/webhook-teslimlerini-listele), [Teslim denemelerini listele](https://developer.siparis.app/uc/teslim-denemelerini-listele) ile okuyun. [Webhook denemesi gönder](https://developer.siparis.app/uc/webhook-denemesi-gonder) yeni deneme kuyruğa alır; çağrının başarılı olması teslimin tamamlandığı anlamına gelmez.

## İmzayı ham gövdeden doğrulayın

Başlıklar `webhook-id`, `webhook-timestamp`, `webhook-signature`. Standard Webhooks biçimi kullanılır: imzalanan baytlar `webhook-id + "." + webhook-timestamp + "." + ham gövde`; algoritma HMAC-SHA256, anahtar `whsec_` sonrası Base64 çözülmüş baytlardır.

JSON’u yeniden yazıp imza hesaplamayın. Önce ham baytları alın; zamanın geçmiş veya gelecekte en çok 300 saniye uzakta olduğunu kontrol edin. İmza başlığında boşlukla ayrılan `v1,<base64>` imzalardan en az birini sabit zamanlı karşılaştırmayla doğrulayın. Sonra JSON’u ayrıştırın. Aşağıdaki dört örnek yalnız imza doğrulamasıdır; kendi HTTP çatınızda başlıkları ve ham gövdeyi bu işleve verin.

### TypeScript ile imza doğrulama

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(raw: Buffer, id: string, timestamp: string,
                       signatures: string, secret: string): void {
  if (!/^\d{1,12}$/.test(timestamp) || !secret.startsWith("whsec_")) throw Error("Geçersiz başlık");
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > 300) throw Error("Eski veya gelecekteki istek");
  const expected = createHmac("sha256", Buffer.from(secret.slice(6), "base64"))
    .update(`${id}.${timestamp}.`).update(raw).digest();
  const valid = signatures.split(" ").some((entry) => {
    const [version, value] = entry.split(",");
    if (version !== "v1" || !value) return false;
    const actual = Buffer.from(value, "base64");
    return actual.length === expected.length && timingSafeEqual(actual, expected);
  });
  if (!valid) throw Error("İmza geçersiz");
}
// raw: JSON ayrıştırılmadan önce alınan Buffer.
// Başlıklar: webhook-id, webhook-timestamp, webhook-signature.
// İmza doğrulaması return.requested/approved/rejected/received/closed ve invoice.issued/failed/cancelled için de aynıdır; olay kimliğiyle tekilleştirin.
// Event.type: payment.failed (payments:read), inventory.low (inventory:read), webhook.test.
// Event.type: subscription.* (subscriptions:read), coupon.* (coupons:read), channel.order_imported/problem/recovered (channels:read).
// webhook.test otomatik denemedir; abonelik seçimi gerekmez, GET /events akışında yoktur.
```


### Python ile imza doğrulama

```python
import base64
import hashlib
import hmac
import re
import time


def verify(raw: bytes, event_id: str, timestamp: str,
           signatures: str, secret: str) -> None:
    if not re.fullmatch(r"[0-9]{1,12}", timestamp) or not secret.startswith("whsec_"):
        raise ValueError("Geçersiz başlık")
    if abs(int(time.time()) - int(timestamp)) > 300:
        raise ValueError("Eski veya gelecekteki istek")
    key = base64.b64decode(secret[6:], validate=True)
    message = f"{event_id}.{timestamp}.".encode() + raw
    expected = base64.b64encode(hmac.new(key, message, hashlib.sha256).digest()).decode()
    if not any(hmac.compare_digest(entry[3:], expected)
               for entry in signatures.split(" ") if entry.startswith("v1,")):
        raise ValueError("İmza geçersiz")
# raw = request.get_data() (Flask) / await request.body() (Starlette).
# İmza doğrulaması return.requested/approved/rejected/received/closed ve invoice.issued/failed/cancelled için de aynıdır; olay kimliğiyle tekilleştirin.
# Event.type: payment.failed (payments:read), inventory.low (inventory:read), webhook.test.
# Event.type: subscription.* (subscriptions:read), coupon.* (coupons:read), channel.order_imported/problem/recovered (channels:read).
# webhook.test otomatik denemedir; abonelik seçimi gerekmez, GET /events akışında yoktur.
```


### PHP ile imza doğrulama

```php
<?php
function verifyWebhook(string $raw, string $id, string $timestamp,
                       string $signatures, string $secret): void {
    if (!preg_match('/^[0-9]{1,12}$/D', $timestamp) || !str_starts_with($secret, 'whsec_')) {
        throw new RuntimeException('Geçersiz başlık');
    }
    if (abs(time() - (int)$timestamp) > 300) {
        throw new RuntimeException('Eski veya gelecekteki istek');
    }
    $key = base64_decode(substr($secret, 6), true);
    if ($key === false) throw new RuntimeException('Sır geçersiz');
    $expected = base64_encode(hash_hmac('sha256', $id.'.'.$timestamp.'.'.$raw, $key, true));
    foreach (explode(' ', $signatures) as $entry) {
        if (str_starts_with($entry, 'v1,') && hash_equals($expected, substr($entry, 3))) return;
    }
    throw new RuntimeException('İmza geçersiz');
}
// $raw = file_get_contents('php://input'); JSON'a çevirmeden doğrulayın.
// İmza doğrulaması return.requested/approved/rejected/received/closed ve invoice.issued/failed/cancelled için de aynıdır; olay kimliğiyle tekilleştirin.
// Event.type: payment.failed (payments:read), inventory.low (inventory:read), webhook.test.
// Event.type: subscription.* (subscriptions:read), coupon.* (coupons:read), channel.order_imported/problem/recovered (channels:read).
// webhook.test otomatik denemedir; abonelik seçimi gerekmez, GET /events akışında yoktur.
```


### C# ile imza doğrulama

```csharp
using System;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

public static class SiparisWebhook {
    public static void Verify(byte[] raw, string id, string timestamp,
                              string signatures, string secret) {
        if (timestamp.Length is < 1 or > 12 || timestamp.Any(c => c < '0' || c > '9')
            || !long.TryParse(timestamp, out var ts) || !secret.StartsWith("whsec_"))
            throw new ArgumentException("Geçersiz başlık");
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (ts < now - 300 || ts > now + 300) throw new ArgumentException("Eski veya gelecekteki istek");
        var prefix = Encoding.UTF8.GetBytes($"{id}.{timestamp}.");
        var message = new byte[prefix.Length + raw.Length];
        prefix.CopyTo(message, 0);
        raw.CopyTo(message, prefix.Length);
        var expected = HMACSHA256.HashData(Convert.FromBase64String(secret[6..]), message);
        foreach (var entry in signatures.Split(' ')) {
            if (!entry.StartsWith("v1,")) continue;
            try {
                var actual = Convert.FromBase64String(entry[3..]);
                if (CryptographicOperations.FixedTimeEquals(actual, expected)) return;
            } catch (FormatException) { }
        }
        throw new ArgumentException("İmza geçersiz");
    }
}
// Request.Body içeriğini byte[] olarak, JSON ayrıştırmadan önce alın.
// İmza doğrulaması return.requested/approved/rejected/received/closed ve invoice.issued/failed/cancelled için de aynıdır; olay kimliğiyle tekilleştirin.
// Event.type: payment.failed (payments:read), inventory.low (inventory:read), webhook.test.
// Event.type: subscription.* (subscriptions:read), coupon.* (coupons:read), channel.order_imported/problem/recovered (channels:read).
// webhook.test otomatik denemedir; abonelik seçimi gerekmez, GET /events akışında yoktur.
```



## Teslimi kalıcı kaydedin

İmzayı doğruladıktan sonra olayı güvenilir kuyruğunuza/veritabanınıza kaydedin ve hızlıca 2xx verin; iş yükünü HTTP yanıtını bekletmeden işleyin. Aynı olay birden fazla gelebilir, sıra garanti edilmez; `evt_…` kimliğiyle tekilleştirin. Güncel durumu kaynak API’sinden okuyun. Tekilleştirme kaydıyla iş etkisi aynı işlemde olsun; hatalı imzaya 2xx dönmeyin.

Başarısız teslimler en çok 11 kez denenir: hemen, 5 saniye, 1 dakika, 5 dakika, 30 dakika, 2 saat, 6 saat, 12 saat, 24 saat, 48 saat, 72 saat; aralıklar ±%20 değişebilir. Yönlendirmeler izlenmez. 400/401/403 erişim ayarı hatasıdır; alıcı ayarını düzeltin. 410 adresi hemen durdurur. 429/502/504 üzerindeki saniye biçimli `Retry-After` bekleme hesabına katılır. Platformun kendi kesintisi sayılmadan 72 saat başarısız deneme sürerse gönderimler durur. Kalıcı teslim özeti korunur; deneme ayrıntıları 90 gün tutulur. Kurtarma için olay akışındaki kalıcı imleci kullanın; [hız ve tekrar kuralları](https://developer.siparis.app/hiz-siniri) ayrıca istemci API isteklerini anlatır.

Sırrı [Webhook sırrını yenile](https://developer.siparis.app/uc/webhook-sirrini-yenile) ile döndürün; `overlap_hours` varsayılan 24, aralık 1–168 saattir. Geçişte eski ve yeni sırla imzalar gelebilir; alıcı ikisini kabul etsin. Adres veya marka değişimi yeniden deneme gerektirir; sır değişimi ayrı döndürme işlemidir. Kapatmak geçmişi silmez. Sorun çözüldükten sonra [Olayı yeniden gönder](https://developer.siparis.app/uc/olayi-yeniden-gonder) ile `event` kimliğini göndererek tekrar teslim isteyebilirsiniz; tekilleştirme sürmelidir.
