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.