For AI agents: the complete documentation index is available at https://docs.halo.run/llms.txt, the full documentation bundle is available at https://docs.halo.run/llms-full.txt, and this page is available as Markdown at https://docs.halo.run/developer-guide/shop/subscription-webhook.md.

订阅 Webhook

适用范围

本页适用于 Halo 商城版 2.27.0 及以上版本。Webhook 的创建与投递记录查看见商城 / Webhook,订阅业务语义见订阅生命周期

投递格式

Halo 以 POST 请求把事件投递到接入方配置的回调 URL,请求体是统一信封:

{
  "eventType": "SUBSCRIPTION_CREATED",
  "timestamp": "2026-09-17T10:00:00Z",
  "webhookId": 1,
  "data": {
    "subscription": {}
  }
}
字段说明
eventType事件类型,例如 SUBSCRIPTION_CREATED
timestamp载荷生成时间(ISO-8601 UTC)。
webhookIdWebhook 配置的 ID(数字),不是本次投递的 ID。
data事件数据,订阅事件固定为 data.subscription

请求头:

请求头说明
X-Halo-Event事件类型,便于路由
X-Halo-Signature-256sha256=<hex>,对原始请求体计算的 HMAC-SHA256
X-Halo-Delivery-Timestamp本次投递时间(Unix 秒),不参与签名
X-Halo-Webhook-Id本次投递的 ID(UUID),重试与手动重投保持不变
X-Halo-Delivery-Attempt当前投递次数,从 1 开始
User-AgentHalo-Webhook/1.0

验证签名

使用创建 Webhook 时填写的密钥,对未经解析的原始请求体验签,并使用常量时间比较:

import hashlib
import hmac

expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received_signature, expected):
    raise ValueError("invalid webhook signature")
注意

X-Halo-Delivery-Timestamp 不参与签名,签名本身不提供防重放能力;如需防重放,请自行基于该请求头做时间窗校验。另外不要先解析再重新序列化 JSON,字节变化会导致验签失败。

返回状态与重试

端点返回Halo 的处理
2xx投递成功
4xx判定为请求或配置错误,立即终止,不再重试
5xx3xx 或网络错误、超时重试,间隔约 1 分钟、5 分钟、30 分钟、2 小时、8 小时,最多投递 6 次(约 10 小时窗口)

Halo 等待响应的超时时间为 10 秒。请先完成验签与持久化,尽快返回 2xx,耗时处理放到后台任务。投递失败后可以在控制台的投递记录中查看请求与响应,并手动重新投递。

幂等与顺序

  • 采用至少一次投递:自动重试与手动重投都可能产生重复请求。请以 X-Halo-Webhook-Id 作为幂等键,处理过的 ID 直接返回 2xx
  • 同一订单(或同一订阅)的事件通常按发布顺序投递,但重试不受顺序约束,可能出现交错。请以订阅的当前快照收敛本地状态,不要假设事件严格有序。
  • 同一逻辑事件的多次投递共享同一个 X-Halo-Webhook-Id;手动重新投递时 X-Halo-Delivery-Attempt 会重置为 1。

订阅事件参考

事件触发时机建议动作
SUBSCRIPTION_CREATED购买支付成功后开通订阅effectiveEntitlements 开通权益
SUBSCRIPTION_TRIAL_STARTED按试用价开通订阅开通试用权益,记录 trialEndAt
SUBSCRIPTION_TRIAL_CONVERTED试用转正支付完成切换为付费档次权益
SUBSCRIPTION_RENEWAL_REMINDER到期前 renewalLeadDays用自有渠道提醒客户续费;同一到期时点只发一次
SUBSCRIPTION_RENEWAL_ORDER_CREATED客户发起续费并生成续费单如需代付,可通过变更流水取到 orderId
SUBSCRIPTION_RENEWED续费支付完成,覆盖终点顺延刷新周期与到期时间,按自有规则重置额度
SUBSCRIPTION_PLAN_CHANGED计划变更已生效用新快照替换本地权益;周期已重置,按新的 paidThroughAt 更新到期时间
SUBSCRIPTION_QUANTITY_CHANGED订阅数量发生变化按新快照刷新权益
SUBSCRIPTION_CHANGE_APPLIED变更单已应用审计与对账;常与 PLAN_CHANGED 一同出现
SUBSCRIPTION_CANCEL_SCHEDULED客户勾选到期取消当期仍然有效;标记本地「不再续费」
SUBSCRIPTION_CANCELLED订阅已取消立即停用业务权益
SUBSCRIPTION_PAST_DUE到期未续费,进入宽限期可降级为只读或限流,或等待宽限结束
SUBSCRIPTION_EXPIRED已过期(宽限结束、预付一期到期或试用过期)立即停用业务权益
暂不投递的事件

SUBSCRIPTION_RENEWAL_FAILEDSUBSCRIPTION_CHANGE_FAILED 会出现在控制台的事件列表中,但当前版本不会产生投递:支付失败不会改变订阅状态,PAST_DUE 仅由到期时间驱动。

试用转正

试用中的订阅调用续费入口时生成的是转正单,不会触发 SUBSCRIPTION_RENEWAL_ORDER_CREATED;转正支付完成后会触发 SUBSCRIPTION_TRIAL_CONVERTED

载荷字段

所有订阅事件的 data.subscription 结构一致,是事件发生之后的订阅快照,不包含订单号与变更前后明细:

字段类型说明
id数字订阅 ID
customerId数字客户 ID
productId数字产品线 ID
planId数字当前计划 ID
variantId数字当前计划对应的商品规格 ID
status字符串TRIALING / ACTIVE / PAST_DUE / CANCELLED / EXPIRED
quantity数字订阅数量
cancelAtPeriodEnd布尔是否已勾选到期取消
trialStartAt / trialEndAt时间或空试用起止
currentPeriodStartAt / currentPeriodEndAt时间或空当前周期起止
paidThroughAt时间或空已付费覆盖终点;买断为 null
effectiveEntitlements对象或空权益契约快照

需要订单号、差价明细或变更前后的周期时,请用 Console 查询接口补全:续费单、转正单与变更单本身也是订单,会同时触发订单域事件。

关联的订单事件

订阅的续费单、转正单与变更单都是普通订单,因此还会触发订单域 Webhook:

  • ORDER_CREATED:订单创建。
  • ORDER_PAID:订单支付完成。应付金额为 0 的订单在创建时即视为已支付,会同时触发 ORDER_CREATEDORDER_PAID
  • FULFILLMENT_REQUESTED:需要接入方交付订阅行时的交付请求,见履约回调

订单载荷中的 data.order.items[].subscriptionMetadata 可以区分订单来源:

字段说明
typePURCHASE / RENEWAL / PLAN_CHANGE / TRIAL_CONVERT
planId相关计划 ID
changeId关联的变更 ID(变更单)
fromQuantity / toQuantity变更前后的数量
feeType费用类型,如 TRIAL / FREE / FIXED_FEE / PRORATED
planName / billingPeriod / billingMode下单时的计划快照

处理建议

  1. 验签 → 用 X-Halo-Webhook-Id 去重 → 用 data.subscription 覆盖本地快照 → 按新的 statusentitlements 调整限额。
  2. 开通与关闭权益必须幂等,重复投递不得叠加额度。
  3. 以 Halo 下发的快照为准,不要在本地自行推算周期,详见权益如何判定
  4. 控制台的发送测试事件会投递 WEBHOOK_TEST,载荷是订单结构且字段与真实事件不完全一致(例如收货地址使用的是测试字段),请勿写入业务数据。