投递行为

标准载荷、5 秒时限、不重试的原因,以及每种投递状态的含义。

Webhook 投递机制刻意保持简单,而这种设计会直接影响您的接收端:仅尝试 1 次 HTTP 请求,限时 5 秒,不重试。在决定处理程序同步执行多少工作之前,请先阅读本页。

标准负载

除 listing.submission 外,所有事件都采用相同的顶层结构,事件特有的字段嵌套在 data 下。

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"  }}
字段类型说明
eventstring以点分隔的事件名称。根据此字段执行分支逻辑。
timestampstringISO-8601 格式的 UTC 时间,记录的是构建投递请求的时间,而非实际变更发生的时间。
account_idinteger事件所属的账户。
location_idstring数字字符串:"279381",而非 279381。interaction.* 不会进行类型强制转换;请参阅下文的例外情况。
dataobject事件特有的字段。事件参考中按事件分别说明。
agency_account_idinteger仅当账户由上级代理机构账户管理时才存在。

data 中的可选字段为空时会被省略,而不是以 null 发送。请采用防御性编程方式:对缺失的键和显式的 null 采用相同的处理方式,切勿假定文档中标为可选的键一定存在。

2 种例外情况

5 秒时限

每次投递仅发送 1 次 POST,连接超时和读取超时均为 5 秒。如果您的端点未及时响应,此次尝试将被放弃,并记录为 timeout。

此时限涵盖整个处理程序的执行过程。唯一安全的设计是:

  1. 验证签名。
  2. 将负载写入持久化存储,例如队列、表或日志。
  3. 返回 200。

其他所有操作(补充数据、分发、回调 REST API,以及任何涉及第三方的操作)都应在从该队列取出数据后执行。

不进行重试

每次投递只尝试 1 次。没有退避机制、重试队列或死信队列。如果您的端点停机 2 分钟,这 2 分钟内触发的事件就会丢失。它们不会在稍后送达。

需要明确说明的 2 个结果:

  • 不会跟随重定向。重定向已完全禁用,因此您的端点返回 301 或 302 时,会被记录为投递失败,不会继续请求另一个 URL。请将您的 webhooks URL 配置为最终 URL。请留意许多 Web 服务器默认添加的从裸域名到 www 的重定向,以及添加尾部斜杠的重定向。
  • 只有 2xx 才算投递成功。3xx、4xx 和 5xx 都会被记录为失败。

重复事件与顺序

  • 您可能会收到同一事件 2 次,也可能在 2 次不同的变更中收到相同的 id。data 中的标识符(交互 id、帖子条目 id、已连接账号 id)标识的是资源,而不是变更。每次评价发生变更时,interaction.review 都会以相同的交互 id 再次触发,而且所有载荷都不包含版本或更新时间字段,因此,如果处理程序根据 id + event 永久去重,就会在不提示的情况下丢弃实际发生的编辑。应改为检测完全相同的重复事件:对原始请求体字节计算哈希值,并在有明确时限的短时间窗口内忽略哈希值相同的重复事件(以分钟计,而非永久)。同一 id 对应的请求体如果不同,就表示一次新的变更,而不是重复事件。然后通过 REST API 回读资源,并以读取结果为准;写入时以资源 id 为键,而不是跳过写入。
  • 不保证事件顺序,timestamp 也无法解决这一问题。事件来自多个独立通道,并在到达时投递,因此,local_post.published 可能先于您预期先收到的 local_post.created 到达。不要根据 timestamp 对同一资源的 2 次变更排序:该时间戳是在构建投递内容时记录的,而不是在变更发生时记录的,并且载荷中没有任何版本号或序列号。如果顺序很重要,请回读资源并使用 API 返回的结果。
  • profile.updated 采用防抖机制,每个地点每 60 秒最多投递 1 次,并在窗口起始时触发。这会直接合并事件,而不是延迟投递:首次编辑开启窗口并触发投递,窗口内的每次后续编辑都会被丢弃,永远不会送达,而 changed_fields 仅描述开启该窗口的那次编辑。因此,您会在窗口开始时收到事件,由事件到达触发的回读只能看到首次编辑。请安排在窗口关闭后进行第 2 次回读,即至少在事件发生 60 秒后,并定时核对地点数据,因为通知本身也可能丢失:仅尝试投递 1 次,不会重试。其他事件均不采用防抖机制。

遗漏事件后的恢复

系统不会重试,也不支持重放,因此每个事件类别都需要有一种可作为后备方案的权威数据读取方式。请定期核对数据,不要仅在发现异常时才核对。

事件类别回读方式建议间隔读取无法恢复的内容
listing.submissionGET /locations/{locationId}/listings/premium,分别使用 listingType=premium 和 additional 各读取 1 次:获取事件通知中各站点的 syncStatus 和 listingUrl。每天对每个地点执行 1 次,并在批量编辑地点后执行。旧版载荷既没有 timestamp,也没有 account_id,因此无法确定遗漏事件的发生时间,也无法确定其所属账号。任何读取操作都不会返回 incomplete 提交中的 error_message。
profile.*GET /locations、GET /locations/search 或 GET /locations-by-ids。每个地点上的 updatedAt 可让您确定哪些地点发生了变更。每小时 1 次,另加上述窗口关闭后的回读。source(user / api / bulk_edit)和 changed_fields:读取只能显示当前状态,无法显示谁更改了什么。被 60 秒防抖机制合并的编辑不会留下任何痕迹,已删除地点的删除前状态也不再保留。
connection.*使用 GET /connected-accounts 和 GET /connected-accounts/{connectedAccountId}/details 读取连接、断开连接和身份验证健康状态。对于 Google 验证相关的 2 个事件,可从任意地点读取结果中获取 googleVerificationStatus { status, message }。每小时 1 次;验证进行期间每隔几分钟 1 次。failure_type(rejected 与 transient 的区别)以及 reason 字符串。无法看出连接是否曾反复断开和恢复:连接断开后又恢复时,读取结果会显示为已连接。
interaction.*按地点使用 GET /locations/{locationId}/reviews,跨地点使用 GET /rollup_interactions,按 id 使用 GET /reviewDetails。每 15 分钟检查新评价,每天进行 1 次更大范围的检查。发生过变更这一事实。文档中列出的交互字段不包含更新时间,因此,编辑后的评价在结构上与您已存储的评价完全相同:请比较您保留的字段。
local_post.*使用 GET /locations/{locationId}/posts、GET /locations/{locationId}/bulk-posts、GET /posts/{postId} 获取各站点当前的发布状态。帖子发布进行期间每小时 1 次。投递时的 rejection_reason,以及帖子条目经历的状态变化顺序。读取返回的是当前状态,而不是状态转换过程。
review_analytics.*使用 GET /locations/{locationId}/review-analytics-overview,并传入该时段的 startDate 和 endDate。每天在汇总窗口结束后执行 1 次;对于每周事件类别,每周执行 1 次。截至快照时点的数值。概览会在读取时重新计算,因此,延迟到达或被删除的评价会使其与快照中的数值不同。

投递结果

每次尝试都会记录状态,即使请求根本没有发出也不例外。以下是具体的状态字符串。

4 种结果及其对应的状态字符串。“已阻止”和“已限流”表示根本没有发出 HTTP 请求,因此应将它们与实际的投递失败区分开来。
状态结果含义处理方式
success投递成功您的端点返回了 2xx。无需操作。
以字符串表示的 HTTP 状态码,e.g。"404"、"500"失败您的端点返回了该非 2xx 状态码。3xx 也属于此类,因为系统不会跟随重定向。检查路径、重定向链和处理程序的错误率。
999失败收到了响应,但其中没有可用的状态码。通常与您端点前的代理或负载均衡器有关。
timeout失败在 5 秒时限内未收到响应。将处理工作移出请求处理流程,并尽早返回确认响应。
network_error失败无法解析或访问主机。检查 DNS,并确认主机可以通过公共 DNS 解析。
error失败此次尝试引发了上述类型之外的异常,例如 TLS 握手失败。检查您的证书链和服务器日志。
blocked_url已阻止URL 在建立连接时未通过安全检查:未使用 HTTPS、缺少主机,或解析到了私有地址、环回地址、链路本地地址或元数据地址。将 URL 指向可公开访问的 HTTPS 主机。请注意,每次投递都会重新执行此检查,因此 DNS 变更可能导致此前可用的 URL 开始被阻止。
endpoint_unverified已阻止该账户已设置签名密钥,但端点尚未通过验证,或验证已被重置。在控制台中重新执行 验证端点。
rate_limited已限流已超出每分钟或每天的上限。事件已被丢弃。减少用量,或改用上限更高的套餐。请参阅套餐和限额。

接收端检查清单

  • 验证每个请求的签名;如果签名不匹配,返回 401,且不执行任何处理。
  • 对原始请求体字节计算哈希,切勿对重新序列化的对象计算哈希。
  • 在远少于 5 秒的时间内返回 2xx;将处理工作加入队列。
  • 以 data 中的 ID 作为处理程序每次写入的键,使重复处理产生相同的结果,但切勿将重复的 id 视为无需执行任何操作。
  • 不要根据到达顺序推断事件顺序,也不要按 timestamp 排序。
  • 在接收数据时,将 location_id 统一转换为字符串。
  • 忽略无法识别的 event 值,并仍然返回 200。
  • 确保 webhooks URL 是最终地址,不发生任何重定向。
  • 定期通过 REST API 核对数据;系统不会重试来弥补遗漏。