İçeriğe geç
siparis.app geliştiriciAnahtarları yönet ↗

Webhook kurmak ve doğrulamak

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 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, Teslim denemelerini listele ile okuyun. Webhook denemesi gönder 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

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

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

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ı ayrıca istemci API isteklerini anlatır.

Sırrı Webhook sırrını 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 ile event kimliğini göndererek tekrar teslim isteyebilirsiniz; tekilleştirme sürmelidir.