使用 webhook 回應事件
設定您的 webhook URL、通過驗證挑戰、驗證每個簽章、在 5 秒內確認接收,並透過輪詢補回遺漏的事件。
當您的帳戶發生變更時,webhook 會將 HTTP POST 推送至您的伺服器,例如收到評論、商家資訊上線或貼文發布。每個帳戶只有一個 URL,且無法依個別事件訂閱,因此同一個接收端會收到文件記載的全部 18 種事件,並依據 event 欄位分流處理。
1. 在儀表板中設定 URL 和密鑰
請先依照步驟 2 和 3 建置接收端。產生密鑰後,簽章將成為必要條件,而且在您的端點通過驗證之前,傳送作業會暫停,因此尚未準備就緒的端點會阻擋所有事件。
- 部署 HTTPS 端點。系統會拒絕私人位址,且不會跟隨重新導向,因此請儲存最終 URL,確保抵達該 URL 前不需經過
www或尾端斜線重新導向。2. 在儀表板的 Webhook 頁面儲存 URL。3. 產生簽章密鑰。密鑰只會顯示一次。請在您的伺服器上將其儲存為LISTINGSAPI_WEBHOOK_SECRET。4. 按一下「驗證端點」。平台會傳送步驟 2 中的挑戰,並等待最多 10 秒。
變更 URL 或重新產生密鑰都會重設驗證狀態,因此請先部署新密鑰,再重新驗證。詳細資訊請參閱 Webhook 概覽。
2. 回應驗證挑戰
挑戰是一個帶有簽章的 POST,其中包含保留的事件名稱和一個 nonce。請回傳 200,並附上僅針對 nonce 計算的 HMAC-SHA256,以小寫十六進位表示。
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}3. 驗證每次傳送的簽章
每個帶有簽章的請求都包含 X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body>。請針對您收到的原始位元組計算相同的摘要,以固定時間進行比較,並在不相符時回傳 401。請勿先剖析 JSON 再重新序列化;這會改變位元組。下方的兩個接收端都會保留原始主體、驗證簽章、回應挑戰,並在回覆前將事件持久化儲存。
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}, 200import 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 });});如果帳戶從未產生過密鑰,收到的請求就不會帶有簽章。請勿以此模式為基礎建置;上述接收端上線後,請產生密鑰。
4. 快速確認接收並以非同步方式處理
每次傳送都是一個 POST,連線逾時和讀取逾時皆為 5 秒,而且只會嘗試一次。系統不會重試、不會退避,也不會重播。如果您的處理常式耗時 6 秒,或您的伺服器停機 2 分鐘,這些事件就會遺失。只有 2xx 才算傳送成功;3xx、4xx 和 5xx 都算失敗。
因此,請求處理流程只做三件事:驗證、將原始主體寫入佇列或資料表並等待提交完成,以及回傳 200。資料補充、回頭呼叫 REST API,以及任何涉及第三方的作業,都從佇列執行。除了 listing.submission 以外,所有事件都使用相同的承載資料結構:
{ "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 是數字字串,但在 interaction.* 事件中會直接傳遞而不經正規化,且可能是 null。listing.submission 沒有 timestamp 或 account_id,並將 status 和 live_link 放在最上層。data 中的選填欄位會省略,不會以 null 傳送。對於任何您無法識別的 event,請回傳 200;文件未記載的事件類別也共用同一個 URL。
5. 去除重複事件並容許事件未依序送達
您可能會收到同一個事件兩次,也會多次收到相同的資源 id:每次評論變更時,interaction.review 都會以相同的互動 id 再次觸發,而且所有承載資料都不含版本資訊。因此,切勿依據 id 加上事件名稱來略過事件,否則會遺漏實際的編輯。請對原始主體計算雜湊值,並忽略短時間內重複出現的相同內容,以偵測完全重複的事件,接著以資源 id 作為寫入的鍵,讓重複事件產生相同的結果。
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事件順序也沒有保證。事件來自各自獨立的處理流程,因此 local_post.published 可能會比 local_post.created 先送達,而 timestamp 記錄的是建立傳送內容的時間,並非變更發生的時間。當順序很重要時,請從 REST API 重新讀取資源,並以該次讀取結果為準。profile.updated 也經過防抖動(debounce)處理,每個地點每 60 秒只產生一個事件,因此在事件送達時重新讀取只會看到第一次編輯;請在此時間範圍結束後再次讀取。
6. 將事件對應至其他指南
請依據 event 分流,並將每個事件交給您為對應指南撰寫的程式碼處理。webhook 告訴您何時發生變更;該指南中的端點則告訴您變更的內容。
每個事件的欄位表格與承載資料範例皆列於事件參考資料。
7. 透過輪詢補救遺漏的事件
由於沒有重試機制,每個事件類別都需要備妥可作為最終依據的讀取方式。請定期核對資料,不要只在看起來有問題時才核對,也絕不可因為未收到事件,就寫入 0 或「無變更」。
儀表板會記錄每次嘗試及其狀態。success 表示已送達;HTTP 狀態碼、timeout、network_error 或 error 表示請求已到達您的端點,但處理失敗;blocked_url、endpoint_unverified 與 rate_limited 表示根本未發出請求,因此需要修正的是設定或方案,而非程式碼。完整清單請參閱傳送行為。
後續步驟
- 擷取評論:大多數整合首先實作的功能類別,並以彙總掃描作為備援機制。
- 檢查商家資訊狀態與已上線連結:
listing.submission通知的內容。 - 參考資料:Webhook 概覽、簽章與驗證、傳送行為、事件參考資料。