Skip to main content
Payven her webhook isteğinde HMAC-SHA256 imza gönderir. Bu imza, isteğin gerçekten Payven’den geldiğini ve request body’sinin değiştirilmediğini garanti eder. Public webhook endpoint’inizi açıyorsanız imza doğrulama zorunludur.

İmza algoritması

Payven her istekte iki header gönderir:
X-Payven-Timestamp: 1714742400
X-Payven-Signature: sha256=4f1d8c92ab7e3bcf9a2e1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f
İmza şu formülle üretilir:
imzalanacak_string = "<X-Payven-Timestamp>" + "." + "<request_body_raw_string'i>"
hmac               = HMAC_SHA256(subscription.secret, imzalanacak_string)
beklenen_imza      = "sha256=" + hex(hmac).lower()
Sizin tarafınızda bu üç adımı uygulayıp X-Payven-Signature header’ı ile karşılaştırırsınız:
  1. Timestamp’in 5 dakika içinde olduğunu kontrol edin (replay koruması)
  2. Body’i string olarak (parse etmeden) HMAC-SHA256 ile imzalayın
  3. Sonucu sabit zamanlı (constant-time) karşılaştırma ile doğrulayın
Body’i parse etmeden imzalayın. JSON.parse() + JSON.stringify() döngüsü property sırasını değiştirip imzayı bozar. Raw string’i mutlaka middleware’den önce okuyun.

Örnek implementasyonlar

import crypto from "crypto";
import express from "express";

const app = express();

// IMPORTANT: imza için ham body gerekir, JSON parse edilmeden saklayın
app.use("/webhooks/payven", express.raw({ type: "application/json" }));

app.post("/webhooks/payven", (req, res) => {
  const signature = req.header("x-payven-signature");
  const timestamp = req.header("x-payven-timestamp");
  const body = req.body.toString("utf8");

  // 1. Timestamp 5 dakika içinde mi?
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return res.status(401).send("Timestamp out of tolerance");
  }

  // 2. İmzayı hesapla
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", process.env.PAYVEN_WEBHOOK_SECRET)
      .update(`${timestamp}.${body}`)
      .digest("hex");

  // 3. Sabit zamanlı karşılaştır
  const sigBuf = Buffer.from(signature ?? "", "utf8");
  const expBuf = Buffer.from(expected, "utf8");
  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(body);
  console.log("Verified event:", event.type, event.id);
  res.status(200).end();
});

Önemli detaylar

Body’i raw olarak okuyun. Express’te express.raw({ type: "application/json" }), ASP.NET’te Request.EnableBuffering() + StreamReader. Framework’ün otomatik JSON parser’ını bypass edin veya parse’tan önce raw bytes’ı kaydedin.
Timestamp toleransı 5 dakika (±300 saniye). Daha gevşek tolerance replay attack riski yaratır.
Sabit zamanlı karşılaştırma kullanın (crypto.timingSafeEqual, hmac.compare_digest, CryptographicOperations.FixedTimeEquals, hash_equals). == ile karşılaştırma timing attack açığı yaratır.
Secret’ı environment variable olarak saklayın, public repo’ya commit etmeyin.
Ham UTF-8 byte’larıyla imzalayın. Pretty-print, BOM, satır sonu farklılıkları imzayı kıracaktır.

Secret rotasyonu

Bir webhook subscription’ın secret’ını rotasyonu için:
curl -X POST https://vpos.payven.com.tr/api/v1/webhook-subscriptions/8e3f5c12-.../rotate-secret \
  -H "Authorization: Bearer $PAYVEN_TOKEN"
Yanıt yeni secret’ı döner. Eski secret 24 saat boyunca geçerli kalır — bu sürede her iki secret ile imzalanmış istekler kabul edilir, böylece zero-downtime geçiş yaparsınız:
// Geçiş döneminde her iki secret'ı dene
const valid =
  verify(body, signature, oldSecret) ||
  verify(body, signature, newSecret);

İmza bozuk geliyorsa

SorunÇözüm
Body parse + re-stringify ediliyorapp.use(express.raw()) veya raw body capture middleware ekleyin
Timestamp drift > 5 dakikaSunucu saatinizi NTP ile senkronize edin
Boşluk / satır sonu farkBody’i Buffer veya bytes olarak saklayın, string dönüşümlerinde encoding belirtin (utf8)
Secret yanlışKonsoldan whsec_ ile başlayan değeri tam olarak kopyaladığınızdan emin olun
X-Payven-Signature boşReverse proxy header strip yapıyor olabilir — Nginx config’de proxy_pass_request_headers on
Hâlâ çözemediğiniz durumda X-Payven-Delivery-Id ile birlikte destek ekibimize yazın.