Webhook'larla olaylara tepki verin

Webhook URL'nizi ayarlayın, doğrulama testini geçin, her imzayı doğrulayın, 5 saniye içinde alındı yanıtı verin, kaçan olayları düzenli sorguyla kurtarın.

Webhook'lar, hesabınızda bir şey değiştiğinde sunucunuza bir HTTP POST gönderir: bir yorum gelir, bir işletme kaydı yayına girer, bir gönderi yayımlanır. Her hesap için tek bir URL vardır ve olay bazında abonelik yoktur; bu nedenle tek bir alıcı, belgelenmiş 18 olayın tamamını alır ve event alanına göre farklı işlem yolları izler.

1. Kontrol panelinde URL'yi ve sırrı ayarlayın

Önce 2. ve 3. adımlardaki alıcıyı oluşturun. Bir sır oluşturmak, imzalamayı zorunlu kılar ve uç noktanız doğrulamayı geçene kadar teslimatı duraklatır; bu nedenle hazır olmayan bir uç nokta tüm olayları engeller.

  1. HTTPS uç noktasını dağıtın. Özel adresler reddedilir ve yönlendirmeler izlenmez; bu nedenle öncesinde www veya sondaki eğik çizgiden kaynaklanan bir yönlendirme bulunmayan nihai URL'yi kaydedin. 2. URL'yi kontrol panelinin Webhook'lar sayfasında kaydedin. 3. Bir imzalama sırrı oluşturun. Bu sır yalnızca bir kez gösterilir. Sunucunuzda LISTINGSAPI_WEBHOOK_SECRET olarak saklayın. 4. Uç noktayı doğrula'ya tıklayın. Platform, 2. adımdaki doğrulama sınamasını gönderir ve en fazla 10 saniye bekler.

URL'yi değiştirmek veya sırrı yeniden oluşturmak doğrulamayı sıfırlar; bu nedenle önce yeni sırrı dağıtın, ardından tekrar doğrulayın. Ayrıntılar Webhook'lara genel bakış sayfasındadır.

2. Doğrulama sınamasını yanıtlayın

Sınama, ayrılmış bir olay adı ve bir nonce içeren, imzalı bir POST isteğidir. Yalnızca nonce değerinin HMAC-SHA256 özetini küçük harfli onaltılık biçimde içeren bir 200 yanıtı verin.

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

3. Her teslimatta imzayı doğrulayın

Her imzalı istek X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body> taşır. Aldığınız baytların aynısı üzerinden aynı özeti hesaplayın, sabit sürede karşılaştırın ve eşleşme yoksa 401 döndürün. Önce JSON'u ayrıştırıp yeniden serileştirmeyin; bu işlem baytları değiştirir. Aşağıdaki iki alıcı da ham gövdeyi korur, imzayı doğrular, sınamayı yanıtlar ve yanıt vermeden önce olayı kalıcı olarak kaydeder.

Flask receiver
import base64import hashlibimport hmacimport os from flask import Flask, abort, request app = Flask(__name__)SECRET = os.environ["LISTINGSAPI_WEBHOOK_SECRET"].encode()  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)  @app.post("/webhooks/listingsapi")def webhooks():  raw = request.get_data()  # raw bytes, exactly as received  if not signature_is_valid(raw, request.headers.get("X-ListingsAPI-Signature")):      abort(401)   body = request.get_json(silent=True) or {}   # One-time handshake: hex digest of the nonce, not the body.  if body.get("event") == "endpoint.verification":      return hmac.new(SECRET, body["nonce"].encode(), hashlib.sha256).hexdigest(), 200   # Real event: durable write first, then acknowledge. Swap for your queue.  inbox.put(raw)  # must be committed before the 200 goes out  return {"received": True}, 200
Express receiver
import crypto from 'crypto';import express from 'express'; const app = express();const SECRET = process.env.LISTINGSAPI_WEBHOOK_SECRET as string; // express.raw keeps req.body as the untouched Buffer the HMAC covers.app.post('/webhooks/listingsapi', express.raw({ type: '*/*' }), async (req, res) => {const raw: Buffer = req.body;const digest = crypto.createHmac('sha256', SECRET).update(raw).digest('base64');const expected = Buffer.from('sha256=' + digest);const given = Buffer.from(req.get('X-ListingsAPI-Signature') ?? '');if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {  return res.status(401).send('bad signature');} const body = JSON.parse(raw.toString('utf8')); // One-time handshake: hex digest of the nonce, not the body.if (body.event === 'endpoint.verification') {  const answer = crypto.createHmac('sha256', SECRET).update(body.nonce).digest('hex');  return res.status(200).send(answer);} // Real event: durable write first, then acknowledge. Swap for your queue.await inbox.put(raw); // must be committed before the 200 goes outreturn res.status(200).json({ received: true });});

Bir hesapta hiç sır oluşturulmamışsa teslimatlar imzasız gelir. Uygulamanızı bu moda dayandırmayın; yukarıdaki alıcı devreye girdikten sonra sırrı oluşturun.

4. Hızlıca alındı yanıtı verin ve asenkron işleyin

Bir teslimat, 5 saniyelik bağlantı ve 5 saniyelik okuma zaman aşımıyla tam olarak bir kez denenen tek bir POST isteğidir. Yeniden deneme, kademeli bekleme veya yeniden oynatma yoktur. İşleyiciniz 6 saniye sürerse veya sunucunuz 2 dakika boyunca çalışmazsa bu olaylar kaybolur. Yalnızca 2xx teslim edilmiş sayılır; 3xx, 4xx ve 5xx başarısızlık sayılır.

Bu nedenle istek işleme akışı yalnızca üç şey yapar: imzayı doğrular, ham gövdeyi bir kuyruğa veya tabloya yazar ve kaydın kesinleşmesini bekler, 200 döndürür. Veri zenginleştirme, REST API'ye yapılan geri çağrılar ve üçüncü taraflarla etkileşim kuran tüm işlemler kuyruktan yürütülür. listing.submission dışındaki tüm olaylar aynı yük yapısını paylaşır:

JSON
{  "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" }}

location_id, normalleştirilmeden aktarıldığı ve null olabildiği interaction.* olayları dışında, sayısal bir dizedir. listing.submission, timestamp veya account_id içermez ve status ile live_link alanlarını en üst düzeye yerleştirir. data içindeki isteğe bağlı alanlar, null olarak gönderilmek yerine atlanır. Tanımadığınız her event için 200 döndürün; belgelenmemiş olay aileleri de aynı URL'yi kullanır.

5. Yinelenen olayları ayıklayın ve sırasız teslimatı tolere edin

Aynı olayı iki kez alabilirsiniz ve aynı kaynak id değerini birçok kez alacaksınız: yorum her değiştiğinde interaction.review, aynı etkileşim id değeriyle yeniden tetiklenir ve hiçbir payload sürüm bilgisi taşımaz. Bu nedenle olayları asla id ve olay adı birleşimine göre yok saymayın; aksi takdirde gerçek düzenlemeleri kaçırırsınız. Ham gövdenin hash değerini hesaplayıp kısa bir zaman aralığında gelen tekrarları yok sayarak birebir kopyaları tespit edin, ardından tekrarlanan bir olayın aynı sonucu üretmesi için yazma işleminizde anahtar olarak kaynak id değerini kullanın.

Exact-duplicate check
import hashlibimport time SEEN: dict[str, float] = {}  # use Redis with a TTL in productionWINDOW_SECONDS = 600 def is_duplicate(raw_body: bytes) -> bool:  key = hashlib.sha256(raw_body).hexdigest()  now = time.time()  for k, seen_at in list(SEEN.items()):      if now - seen_at > WINDOW_SECONDS:          del SEEN[k]  if key in SEEN:      return True  SEEN[key] = now  return False

Sıralama da garanti edilmez. Olaylar bağımsız işlem hatlarından gelir, bu nedenle local_post.published, local_post.created olayından önce ulaşabilir ve timestamp zaman damgası değişiklik gerçekleştiğinde değil, teslimat oluşturulduğunda atanır. Sıralamanın önemli olduğu durumlarda kaynağı REST API üzerinden yeniden okuyun ve bu okumayı esas alın. Ayrıca profile.updated, konum başına 60 saniyede bir olayla sınırlanacak şekilde birleştirilir; bu nedenle olay ulaştığında yapılan yeniden okuma yalnızca ilk düzenlemeyi görür; zaman aralığı sona erdikten sonra tekrar okuyun.

event değerine göre dallanın ve her olayı ilgili kılavuz için yazdığınız koda aktarın. Webhook ne zaman olduğunu, o kılavuzdaki endpoint ise ne olduğunu bildirir.

OlayNe olduOkunacak kılavuz
profile.createdBir konum oluşturuldu. data.source değeri user, api veya bulk_edit olur.Konum ekleme
profile.updatedBir konum düzenlendi. data.changed_fields bir ipucudur, değişikliklerin tam dökümü değildir.Konum güncelleme
profile.deletedBir konum arşivlendi.Konumları bulma ve arşivleme
listing.submissionBir yayın platformuna yapılan gönderim sonuçlandı: status değeri success, incomplete veya canceled olur.İşletme kaydı durumunu ve yayındaki bağlantıları kontrol etme
connection.location_connected, connection.location_disconnectedBir Google veya Facebook işletme kaydı bağlandı ya da bağlantısı kaldırıldı.Google'ı bağlama ve konumları içe aktarma, Facebook Sayfalarını bağlama
connection.reauth_required, connection.listing_inaccessibleKimlik bilgilerinin süresi doldu veya bir işletme kaydına erişim kaybedildi.Google'ı bağlama ve konumları içe aktarma
connection.google_verification_verified, connection.google_verification_failedBir Google doğrulaması tamamlandı.Google Business Profile oluşturma
interaction.reviewBir yorum geldi veya değişti. Alanlar data.interaction altında yer alır.Yorumları getirme
interaction.responseBir işletme sahibi yanıtı yayımlandı, düzenlendi veya arşivlendi. parent_id yorumdur.Yorumları yanıtlama
local_post.created, local_post.published, local_post.rejected, local_post.deletedBir gönderi öğesinin durumu değişti. Anahtar olarak her konum için bir tane olan data.post_item_id değerini kullanın.Gönderi yayımlama
review_analytics.daily_snapshot, review_analytics.weekly_snapshotYorum toplulaştırma işlemi bir konum ve dönem için çalıştırıldı.Yorum analizi

Her olayın alan tabloları ve örnek payload içeriği olay referansında yer alır.

7. Kaçırılan bir olayı periyodik sorgulamayla telafi edin

Yeniden deneme olmadığı için her olay ailesinde, gerektiğinde başvurabileceğiniz ve kesin bilgiyi sağlayan bir okuma işlemi gerekir. Verileri yalnızca bir şeyler yanlış göründüğünde değil, düzenli aralıklarla uzlaştırın ve sırf bir olay gelmedi diye asla sıfır veya "değişiklik yok" yazmayın.

AileYeniden okumak için kullanılacak yöntemAralık
listing.submissionsyncStatus ve listingUrl için GET /locations/{locationId}/listings/premium.Her konum için günlük.
profile.*GET /locations veya GET /locations/search; updatedAt neyin değiştiğini gösterir.Saatte bir, ayrıca her profile.updated olayından 60 saniye sonra.
connection.*GET /connected-accounts ve herhangi bir konum okumasında googleVerificationStatus.Saatte bir; doğrulama sırasında birkaç dakikada bir.
interaction.*Hesap genelinde GET /rollup_interactions, id değerine göre GET /reviewDetails.Yeni yorumlar için 15 dakikada bir, tam tarama için günlük.
local_post.*GET /posts/{postId} veya GET /locations/{locationId}/posts.Gönderiler işlenirken saatte bir.
review_analytics.*Dönemin tarihleriyle GET /locations/{locationId}/review-analytics-overview.Toplulaştırma işleminden sonra günlük.

Kontrol paneli her denemeyi bir durumla kaydeder. success teslim edildiği anlamına gelir; bir HTTP kodu, timeout, network_error veya error, endpoint'inize ulaşıldığı ve işlemin başarısız olduğu anlamına gelir; blocked_url, endpoint_unverified ve rate_limited ise hiç istek yapılmadığı anlamına gelir, dolayısıyla çözüm kodda değil, yapılandırmada veya plandadır. Tam liste Teslimat davranışı bölümünde yer alır.

Sonraki adımlar