İmzalar ve doğrulama

X-ListingsAPI-Signature başlığını doğrulayın, uç nokta doğrulama sınamasına yanıt verin ve base64 ile hex kodlamalarını karıştırmayın.

Webhook URL'niz herkese açık bir HTTPS uç noktasıdır; bu nedenle internetteki herkes bu adrese POST gönderebilir. İmzalama gizli anahtarı, gerçek bir teslimatı sahte bir teslimattan ayırt etmenizi sağlar: imzalanmış her istek, yalnızca sizde ve platformda bulunan bir gizli anahtarla hesaplanmış bir HMAC içerir.

Bu gizli anahtarı iki ayrı mekanizma kullanır ve bunlar kolayca karıştırılabilir:

AmaçYönKodlama
İstek imzasıHer teslimatın gerçek olduğunu kanıtlamakBir üst bilgide size gönderilirbase64
Doğrulama sınamasıGizli anahtarın sizde olduğunu bir kez kanıtlamakGövde içinde geri gönderirsinizhex

İmza üst bilgisi

İmzalanmış her teslimat şunu içerir:

Text
X-ListingsAPI-Signature: sha256=<digest>   digest = base64( HMAC-SHA256(secret, raw_request_body) )

sha256= ön eki, üst bilgi değerinin bir parçasıdır. Beklenen dizeyi oluştururken bu ön eki ekleyin veya yalnızca = sonrasındaki kısmı karşılaştırın. Özet hesaplanmadan önce gövdenin başına hiçbir şey eklenmez: zaman damgası, sürüm veya birleştirilmiş imzalama dizesi eklenmez. HMAC yalnızca ham gövdeyi kapsar.

Adım adım doğrulama

  1. Ham istek gövdesinin baytlarını, tam olarak alındıkları haliyle okuyun. JSON verisini ayrıştırıp yeniden serileştirmeyin. Anahtar sırası, boşluklar ve Unicode kaçış dizileri baytları değiştirir; herhangi bir değişiklik HMAC doğrulamasını bozar. Çoğu uygulama çatısında ham gövdenin saklanması için yapılandırma gerekir. Aşağıdaki örneklere bakın.
  2. HMAC-SHA256(your_secret, raw_body) hesaplayın, base64 ile kodlayın ve başına sha256= ekleyin.
  3. Üst bilgiyle karşılaştırmak için sabit süreli bir karşılaştırma kullanın (crypto.timingSafeEqual, hmac.compare_digest). Basit bir == karşılaştırması, baştan itibaren kaç baytın eşleştiğine ilişkin zamanlama bilgisi sızdırır.
  4. Eşleşme yoksa 401 döndürün ve hiçbir işlem yapmayın.
Alıcı tarafındaki kontrol. Her şey ikinci adıma bağlıdır: HMAC, yeniden kodladığınız herhangi bir veri üzerinden değil, aldığınız baytlar üzerinden hesaplanır.

Uç nokta doğrulaması

Gerçek olaylar gönderilmeye başlamadan önce, uç noktanızın gizli anahtara gerçekten sahip olduğunu bir kez kanıtlaması gerekir. Bu işlem, imzalanmış yüklerin yanlış yazılmış bir URL'ye veya bunları doğrulayamayan birine ait bir URL'ye sürekli olarak gönderilmesini önler.

Bu işlemi kontrol panelindeki Webhooks sayfasından başlatırsınız. Platform, imzalanmış bir sınama gönderir ve yanıt için en fazla 10 saniye bekler. (Bu 10 saniye yalnızca el sıkışması için geçerlidir; gerçek teslimatlar 5 saniyede zaman aşımına uğrar.)

El sıkışması. İki yön arasındaki farka dikkat edin: sınamanın imzası bir üst bilgide base64 olarak gönderilir; yanıtınız ise yanıt gövdesinde bir hex özetidir.

Doğrulama sorgusu sözleşmesi

Doğrulama sorgusu, ayrılmış bir olay adı kullanan sıradan bir imzalı istektir:

JSON
{  "event": "endpoint.verification",  "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c",  "timestamp": "2026-08-25T14:32:10Z"}

Uç noktanızın şunları yapması gerekir:

  1. X-ListingsAPI-Signature değerini herhangi bir teslimatta olduğu gibi doğrulayın.
  2. event == "endpoint.verification" değerini tanıyın ve normal olay işleme akışınızdan önce ayrı bir işleme dalına geçin.
  3. HMAC-SHA256(secret, nonce) değerini küçük harfli onaltılık bir dize olarak hesaplayın. HMAC, istek gövdesi üzerinden değil, yalnızca nonce üzerinden hesaplanır.
  4. Yanıt gövdesinde bu onaltılık dizeyle 200 döndürün. Yalın bir onaltılık dize beklenir; tırnak içine alınmış veya JSON dizesi olarak sunulmuş bir onaltılık değer de kabul edilir.

Bunların dışındaki her durumda (2xx dışı bir durum kodu, yanlış bir özet, zaman aşımı, erişilemeyen bir sunucu) doğrulama başarısız olur.

Doğrulamayı sıfırlayan durumlar

Doğrulama kalıcı değildir. Aşağıdaki durumların her birinde hesap yeniden doğrulanmamış duruma geçer ve teslimatlar duraklatılır:

  • Webhooks URL'sini değiştirdiğinizde veya gizli anahtarı yeniden oluşturduğunuzda. Önceki doğrulama kanıtı yeni yapılandırma için artık geçerli değildir.
  • Bir doğrulama girişimi başarısız olduğunda: Buna, önceden çalışan ancak artık adresi çözümlenemeyen veya yanıt vermeyen bir uç nokta da dahildir. Bu nedenle, çalışmayan bir uç noktada doğrulamayı yeniden çalıştırmak teslimatları durdurur; bu, tasarlanan davranıştır.

Teslimatları sürdürmek için uç nokta düzgün çalışır duruma geldiğinde yeniden doğrulayın.

Çalışan alıcı örnekleri

Her iki örnek de üç görevi yerine getirir: ham gövdeyi korur, imzayı sabit sürede doğrular ve doğrulama sorgusunu yanıtlar. Her ikisi de ortamdan tek bir gizli anahtar okur: Her hesap için tam olarak bir geçerli gizli anahtar vardır ve iki anahtarın aynı anda geçerli olduğu bir süre yoktur; bu nedenle iki anahtardan herhangi birini kabul eden bir döngü oluşturmayın.

Node.js (Express)
const crypto = require('crypto');const express = require('express'); const app = express();const SECRET = process.env.LISTINGSAPI_WEBHOOK_SECRET; // Keep the raw bytes. express.json() would otherwise leave you// with only the parsed object, and re-serializing it changes the// bytes the HMAC covers.app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }),); function signatureIsValid(rawBody, header) {const digest = crypto  .createHmac('sha256', SECRET)  .update(rawBody)  .digest('base64');const expected = 'sha256=' + digest; const a = Buffer.from(header || '', 'utf8');const b = Buffer.from(expected, 'utf8');// timingSafeEqual throws on a length mismatch, so check first.return a.length === b.length && crypto.timingSafeEqual(a, b);} // The only work allowed before the 200: put the raw bytes somewhere durable// and wait for the write to be acknowledged. Swap the INSERT for your real// queue or table write; it must be committed before you respond, because// nothing is retried if your process dies after the 200 goes out.async function storeForProcessing(rawBody, signature) {await db.query(  'INSERT INTO webhook_inbox (signature, body, received_at) VALUES ($1, $2, now())',  [signature, rawBody.toString('utf8')],);} app.post('/webhooks/listingsapi', async (req, res) => {const header = req.get('X-ListingsAPI-Signature');if (!signatureIsValid(req.rawBody, header)) {  return res.status(401).send('bad signature');} const body = req.body; // The verification handshake: answer with HEX, not base64.if (body.event === 'endpoint.verification') {  const answer = crypto    .createHmac('sha256', SECRET)    .update(body.nonce)    .digest('hex');  return res.status(200).send(answer);} // A real event: durable write first, acknowledge second.try {  await storeForProcessing(req.rawBody, header);} catch (err) {  // Nothing was persisted, so do not claim success.  return res.status(500).send('could not persist');}return res.status(200).json({ received: true });});
Python (Flask)
import base64import hashlibimport hmacimport os from flask import Flask, abort, request app = Flask(__name__)SECRET = os.environ["LISTINGSAPI_WEBHOOK_SECRET"].encode() HEADER = "X-ListingsAPI-Signature"  def signature_is_valid(raw_body: bytes, header: str | None) -> bool:  digest = hmac.new(SECRET, raw_body, hashlib.sha256).digest()  expected = "sha256=" + base64.b64encode(digest).decode()  return hmac.compare_digest(header or "", expected)  def store_for_processing(raw_body: bytes, signature: str) -> None:  """The only work allowed before the 200: put the raw bytes somewhere  durable and return only once the write is committed. Swap the INSERT for  your real queue or table write - nothing is retried if the process dies  after the response goes out."""  with db.begin() as tx:      tx.execute(          "INSERT INTO webhook_inbox (signature, body) VALUES (%s, %s)",          (signature, raw_body),      )  @app.post("/webhooks/listingsapi")def webhooks():  raw = request.get_data()  # raw bytes, exactly as received  header = request.headers.get(HEADER)  if not signature_is_valid(raw, header):      abort(401)   body = request.get_json(silent=True) or {}   # The verification handshake: answer with HEX, not base64.  if body.get("event") == "endpoint.verification":      nonce = body["nonce"].encode()      return hmac.new(SECRET, nonce, hashlib.sha256).hexdigest(), 200   # A real event: durable write first, acknowledge second.  try:      store_for_processing(raw, header or "")  except Exception:      # Nothing was persisted, so do not claim success.      return {"error": "could not persist"}, 500  return {"received": True}, 200

Test vektörleri

Sabit girdiler ve doğru bir uygulamanın ürettiği kesin çıktılar. Uç noktanıza gerçek bir teslimat yönlendirmeden önce bunları bir birim testine ekleyin.

Gizli anahtar (açıkça sahte, yalnızca testler için):

Text
whsec_test_0123456789abcdef0123456789abcdef

Ham istek gövdesi, tam olarak şu baytlar:

Text
{"event":"connection.location_connected","timestamp":"2026-08-25T14:32:10Z","account_id":11073,"location_id":"279381","data":{"platform":"google","connected_account_id":"ba03bc4b-9f8c-4d3b-8e93-9628034c63cc"}}

Beklenen istek imzası, base64 biçiminde, sha256= önekiyle:

Text
X-ListingsAPI-Signature: sha256=+FxQAwiCAfQdicZcHlv9fNOrkjf0yK99Jje97VMS6Y4=

Doğrulama sorgusunda, aynı gizli anahtar ve şu nonce kullanıldığında:

Text
9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c

uç noktanız, gövdede tam olarak şu küçük harfli onaltılık değerle 200 döndürmelidir:

Text
d5aff73f9739b49aaabccf264c8777a697998ba07c493fbaec646b5370354145

Her iki sonucu da yeniden üretmek için yukarıdaki gövde satırını sonunda yeni satır karakteri olmadan (printf '%s' '...' > body.json) body.json dosyasına kaydedin, ardından:

Python
import base64, hashlib, hmac secret = b"whsec_test_0123456789abcdef0123456789abcdef"body = open("body.json", "rb").read()          # the 209 bytes abovenonce = b"9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c" # request signature: base64, over the raw bodyprint("sha256=" + base64.b64encode(hmac.new(secret, body, hashlib.sha256).digest()).decode())# challenge answer: lowercase hex, over the nonce aloneprint(hmac.new(secret, nonce, hashlib.sha256).hexdigest())

İlk satır yukarıdaki üstbilgi değeriyle eşleşmiyorsa sorun HMAC hesabınızda değil, baytlarınızdadır. Önce gövde uzunluğunu kontrol edin.

Sonraki