İ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:
İmza üst bilgisi
İmzalanmış her teslimat şunu içerir:
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
- 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.
HMAC-SHA256(your_secret, raw_body)hesaplayın, base64 ile kodlayın ve başınasha256=ekleyin.- Ü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. - Eşleşme yoksa
401döndürün ve hiçbir işlem yapmayın.
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.)
Doğrulama sorgusu sözleşmesi
Doğrulama sorgusu, ayrılmış bir olay adı kullanan sıradan bir imzalı istektir:
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}Uç noktanızın şunları yapması gerekir:
X-ListingsAPI-Signaturedeğerini herhangi bir teslimatta olduğu gibi doğrulayın.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.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.- Yanıt gövdesinde bu onaltılık dizeyle
200dö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.
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 });});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}, 200Test 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):
whsec_test_0123456789abcdef0123456789abcdefHam istek gövdesi, tam olarak şu baytlar:
{"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:
X-ListingsAPI-Signature: sha256=+FxQAwiCAfQdicZcHlv9fNOrkjf0yK99Jje97VMS6Y4=Doğrulama sorgusunda, aynı gizli anahtar ve şu nonce kullanıldığında:
9f2c1e642b1a4b0e9f1a2c7d5e8a1b3cuç noktanız, gövdede tam olarak şu küçük harfli onaltılık değerle 200 döndürmelidir:
d5aff73f9739b49aaabccf264c8777a697998ba07c493fbaec646b5370354145Her 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:
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
- Teslimat davranışı: standart veri yükü, 5 saniyelik süre sınırı ve tüm teslimat durumları.
- Olay referansı: 18 olayın tamamı ve veri yükleri.