Görünüm
Webhook imzası
Webhook adresiniz internete açıktır; oraya herkes istek gönderebilir. İmza, isteğin gerçekten bizden geldiğinin ve yolda değişmediğinin kanıtıdır. Doğrulamayan bir alıcı sahte "sipariş hazır" bildirimlerine açıktır.
İmza zaman damgası taşır: aynı isteğin sonradan tekrar oynatılmasını (replay) ayıklayabilirsiniz.
Ne gelir
http
POST /veni-webhook HTTP/1.1
Host: ornek.com
Content-Type: application/json
User-Agent: Veni-Webhooks/2.0
X-Veni-Event: order.ready
X-Veni-Delivery: 8f3c1e2a-77b0-4c19-9a52-0e3f7d14c6b1
X-Veni-Timestamp: 1786612447
X-Veni-Signature: v1,7b21af04…
{"id":"8f3c1e2a-77b0-4c19-9a52-0e3f7d14c6b1","event":"order.ready","occurred_at":"2026-10-12T09:14:07Z","data":{…}}| Başlık | Ne söyler |
|---|---|
X-Veni-Event | Hangi olay geldi. Gövdedeki event ile aynıdır. |
X-Veni-Delivery | Bu teslimatın kimliği. Yeniden denemelerde değişmez → tekilleştirme anahtarınız. Gövdedeki id ile aynıdır. |
X-Veni-Timestamp | İsteğin gönderildiği an (unix saniye). İmzanın parçasıdır. |
X-Veni-Signature | İmza: v1,<hex>. Boşlukla ayrılmış birden çok değer taşıyabilir (sır yenileme). |
Doğrulama
İmzalanan metin üç parçanın birleşimidir:
<X-Veni-Delivery> + "." + <X-Veni-Timestamp> + "." + <ham gövde>Bunun HMAC-SHA256'sını sırrınızla hesaplar, başına v1, ekler ve başlıkla karşılaştırırsınız.
js
import crypto from "node:crypto";
const TOLERANS_SN = 300; // 5 dakika; eski istekleri ayıklar
/**
* @param {Record<string, string>} h küçük harfli başlıklar
* @param {Buffer} hamGovde isteğin HAM baytları
* @param {string} sir whsec_… ile başlayan sırrınız
*/
export function veniImzasiDogru(h, hamGovde, sir) {
const ts = h["x-veni-timestamp"];
const teslimat = h["x-veni-delivery"];
if (!ts || !teslimat) return false;
// Saat penceresi: çok eski bir isteğin tekrar oynatılmasını engeller.
if (Math.abs(Date.now() / 1000 - Number(ts)) > TOLERANS_SN) return false;
const beklenen =
"v1," +
crypto
.createHmac("sha256", sir)
.update(`${teslimat}.${ts}.`)
.update(hamGovde)
.digest("hex");
// Başlık birden çok imza taşıyabilir; BİRİ tutarsa yeter.
return (h["x-veni-signature"] ?? "")
.split(" ")
.some((gelen) => sabitZamandaEsit(gelen, beklenen));
}
function sabitZamandaEsit(a, b) {
const x = Buffer.from(a);
const y = Buffer.from(b);
return x.length === y.length && crypto.timingSafeEqual(x, y);
}python
import hashlib
import hmac
import time
TOLERANS_SN = 300 # 5 dakika
def veni_imzasi_dogru(basliklar: dict[str, str], ham_govde: bytes, sir: str) -> bool:
ts = basliklar.get("x-veni-timestamp", "")
teslimat = basliklar.get("x-veni-delivery", "")
if not ts or not teslimat:
return False
try:
if abs(time.time() - int(ts)) > TOLERANS_SN:
return False
except ValueError:
return False
imzalanan = f"{teslimat}.{ts}.".encode() + ham_govde
beklenen = "v1," + hmac.new(sir.encode(), imzalanan, hashlib.sha256).hexdigest()
# Başlık birden çok imza taşıyabilir; BİRİ tutarsa yeter.
return any(
hmac.compare_digest(gelen, beklenen)
for gelen in basliklar.get("x-veni-signature", "").split(" ")
)Beş dakikalık pencere bizim zorunlu kıldığımız bir kural değil, öneri. Kendi toleransınızı seçebilirsiniz; ama sunucunuzun saati kaymışsa geçerli istekleri de reddedersiniz — saat eşitlemesi (NTP) açık olsun.
Ham gövde şart
İmza gövdenin baytları üzerinden hesaplanır. JSON'u ayrıştırıp yeniden serileştirirseniz tek bir boşluk ya da alan sırası farkı imzayı bozar. Çoğu çatıda gövdeyi ham almak için ayrı bir ayar gerekir:
js
import express from "express";
const app = express();
// DİKKAT: express.json() DEĞİL — ham baytlar gerekiyor.
app.post("/veni-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!veniImzasiDogru(req.headers, req.body, process.env.VENI_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const olay = JSON.parse(req.body.toString("utf8"));
// Tekilleştirme: aynı teslimat kimliğini daha önce gördüyseniz işlemeyin.
kuyrugaAl(olay);
res.sendStatus(200); // Önce 200 dönün, ağır işi sonra yapın.
});Sır yenileme
X-Veni-Signature başlığı boşlukla ayrılmış birden çok imza taşıyabilir. Doğrulayıcınız başlığı boşluktan bölüp herhangi biri tutuyorsa isteği kabul etmelidir — yukarıdaki örneklerde bu yapılıyor. Yalnız ilk imzayı okuyan bir doğrulayıcı, çok imzalı bir teslimatta isteklerin bir kısmını reddeder.
Restoran aboneliğinde geçiş penceresi yok
POST …/webhooks/{id}/rotate-secret ile yenilenen sır anında yürürlüğe girer ve başlık tek imza taşır.
Kesinti istemiyorsanız sıra: doğrulayıcınız iki sırrı birden kabul etsin → sır yenilenir → yeni sır yerleştirilir → eskisi kaldırılır.
İmza tutmazsa
Reddedin ve 401 dönün. Bizim tarafta bu başarısız bir teslimattır: yeniden denenir, hakkı bitince ulaşamayanlar listesine düşer.
Dikkat: hata sizin doğrulayıcınızdaysa (örneğin gövdeyi yeniden serileştiriyorsanız) bütün teslimatlar reddedilir ve üst üste 20 başarısız denemede abonelik otomatik kapatılır — bir teslimat 6 deneme yaptığı için bu 4 olay, yaklaşık 3 saat demektir. Entegrasyonu açarken ilk olayların geldiğini teslimat geçmişinden doğrulayın.
Deneme olayı
Aboneliğe gerçek imza yolundan sentetik bir olay gönderilebilir: POST …/webhooks/{id}/test. Olay adı webhook.ping, gövdesinde "test": true vardır. Katalogda böyle bir iş olayı yoktur, yani gerçek bir olayla karıştırılamaz. İş mantığınıza sokmayın — yalnız imza ve bağlantı provasıdır.
Deneme gövdesi standart zarfı KULLANMAZ
Başlıklar ve imza gerçek teslimatla birebir aynıdır, yani imza doğrulamanızı buradan sınayabilirsiniz. Ama gövde düzdür: id, occurred_at ve data yoktur.
json
{ "event": "webhook.ping", "test": true, "timestamp": "2026-10-12T09:14:07Z", "message": "…" }Tekilleştirmeyi gövdedeki id üzerinden yapıyorsanız deneme o kod yolunu sınamaz — X-Veni-Delivery başlığını kullanın.
Partner teslimat olayları
Partner anahtarıyla açtığınız teslimatların delivery.* olayları iki noktada farklıdır:
| Partner teslimat olayları | Diğer olaylar | |
|---|---|---|
| Gövde | Düz: {event_type, event_id, occurred_at, delivery, external_ref} | Zarf: {id, event, occurred_at, data} |
Başlıklar ve imza doğrulaması ikisinde de aynıdır; fark yalnız gövde biçimidir.
Sırada
Teslimat takvimi, tekilleştirme ve ulaşamayanlar listesi: Webhook'lar.