订阅 Webhook
本页适用于 Halo 商城版 2.27.0 及以上版本。Webhook 的创建与投递记录查看见商城 / Webhook,订阅业务语义见订阅生命周期。
投递格式
Halo 以 POST 请求把事件投递到接入方配置的回调 URL,请求体是统一信封:
请求头:
验证签名
使用创建 Webhook 时填写的密钥,对未经解析的原始请求体验签,并使用常量时间比较:
签名只覆盖原始请求体,X-Halo-Delivery-Timestamp 与 X-Halo-Webhook-Id 均不参与签名。获得有效旧请求的人可以修改这两个请求头,因此请求头时间窗与投递 ID 去重不能作为防重放保障。
如需防重放,可在验签后持久化原始请求体的摘要以识别重复载荷,并基于正文中已签名的 timestamp 制定接受时间窗。时间窗需要考虑自动重试约 10 小时及更晚的人工重投,超出窗口的通知应通过查询当前业务状态对账处理。业务操作仍须保持幂等,不能因为请求头改变就再次交付或增加额度。
不要先解析再重新序列化 JSON,字节变化会导致验签失败。
返回状态与重试
Halo 等待响应的超时时间为 10 秒。请先完成验签与持久化,尽快返回 2xx,耗时处理放到后台任务。投递失败后可以在控制台的投递记录中查看请求与响应,并手动重新投递。
幂等与顺序
- 采用至少一次投递:自动重试与手动重投都可能产生重复请求。正常投递去重可使用
X-Halo-Webhook-Id,处理过的 ID 直接返回2xx;它不替代防重放校验与业务幂等。 - 同一订单(或同一订阅)的事件通常按发布顺序投递,但重试不受顺序约束,可能出现交错。重试使用保存的原始载荷,不会重新查询订阅;不同事件的 ID 去重也不能阻止旧快照覆盖新状态。
- 当前载荷没有订阅状态的单调递增版本号,正文
timestamp是载荷生成时间,也不能作为状态版本。更新权益时,应按订阅 ID 串行执行「查询当前订阅 → 更新本地快照与权益」,不要直接用每次收到的事件快照覆盖本地状态。查询接口和令牌权限见查询与对账接口;仅有履约上报权限时,需要由有查询权限的运营侧服务完成对账。 - 自动重试与 Webhook 投递记录中的手动重新投递共享原
X-Halo-Webhook-Id,后者的X-Halo-Delivery-Attempt会重置为 1。订单详情页重新发送履约请求会产生新事件和新 ID,详见交付请求通知。
订阅事件参考
SUBSCRIPTION_RENEWAL_FAILED 与 SUBSCRIPTION_CHANGE_FAILED 会出现在控制台的事件列表中,但当前版本不会产生投递:支付失败不会改变订阅状态,PAST_DUE 仅由到期时间驱动。
试用中的订阅调用续费入口时生成的是转正单,不会触发 SUBSCRIPTION_RENEWAL_ORDER_CREATED;转正支付完成后会触发 SUBSCRIPTION_TRIAL_CONVERTED。
载荷字段
所有订阅事件的 data.subscription 结构一致,是载荷生成时查询到的订阅快照,不包含订单号与变更前后明细。延迟或重试投递中的快照可能已经过时:
需要订单号、差价明细或变更前后的周期时,请用 Console 查询接口补全:续费单、转正单与变更单本身也是订单,会同时触发订单域事件。
关联的订单事件
订阅的续费单、转正单与变更单都是普通订单,因此还会触发订单域 Webhook:
ORDER_CREATED:订单创建。ORDER_PAID:订单支付完成。应付金额为 0 的订单在创建时即视为已支付,会同时触发ORDER_CREATED与ORDER_PAID。FULFILLMENT_REQUESTED:需要接入方交付订阅行时的交付请求,见履约回调。
订单载荷中的 data.order.items[].subscriptionMetadata 可以区分订单来源:
处理建议
- 验签并校验重放 → 投递去重并持久化待处理任务 → 按
data.subscription.id串行查询当前订阅 → 用查询结果更新本地快照 → 按当前status与effectiveEntitlements调整限额。事件载荷中的权益字段同样是data.subscription.effectiveEntitlements。 - 开通与关闭权益必须幂等,重复投递不得叠加额度。
- 以 Halo 下发的快照为准,不要在本地自行推算周期,详见权益如何判定。
- 控制台的发送测试事件会投递
WEBHOOK_TEST,载荷是订单结构且字段与真实事件不完全一致(例如收货地址使用的是测试字段),请勿写入业务数据。