# 涨米 Webhook 接入 后台 → Webhook → 添加接收地址。商店通知是“华为 → 涨米”,Webhook 是“涨米 → 你的业务服务器”,两者分别配置。 ## 交付约定 - 公网 HTTPS,JSON POST;仅 HTTP 200 表示成功。连接、TLS、读响应总超时 15 秒,不跟随重定向。 - 新地址仅接收创建后记录的事件。可选应用、生产 NORMAL / 沙盒 SANDBOX / BOTH、事件类型。地址停用会暂停发送,启用后补发符合条件的积压事件。环境与类型筛选作用于事件入队;已入队任务保留原环境和消息内容,修改配置不会改写它们。修改 URL、Authorization 或签名密钥会用于后续发送(包括重试)。更换接收业务时建议新建地址并停用旧地址。 - 持久化队列;首次失败后最多再重试 5 次,间隔 5、10、20、40、80 分钟。仍失败标记 FAILED;后台可手动重试,保留原事件 ID 和历史。 - 至少一次投递,可能重复、延迟或乱序。接收端必须按项目 + event.id 去重,并持久保存后再返回 200。不要以请求签名或投递 ID 作为业务事件唯一标识。 - 测试事件 type=TEST,不改变会员权益;BOTH 地址测试默认使用 SANDBOX,页面会切换到相应环境显示记录。 - 平台发送在独立后台 worker 运行,接收方慢或离线不阻塞购买请求。部署需保持服务进程常驻、数据库持久化、允许出站 HTTPS;webhookWorkersDisabled=true 会停用 worker(仅隔离预览使用)。 ## 签名验证 创建地址时只显示一次 signingSecret。加密保存,列表和编辑接口不回显。轮换后新请求立即使用新密钥,旧密钥失效,需协调更新接收服务器。 请求头: - X-Zhangmi-Event-ID:业务事件 ID。 - X-Zhangmi-Delivery-ID:此接收地址对应的投递任务 ID。 - X-Zhangmi-Webhook-Signature:t=,v1=。 - Authorization:可选,使用配置中的完整值,例如 Bearer ...。 计算签名:HMAC-SHA256(key=signingSecret 的 UTF-8 文本, message=t 的十进制文本 + "." + 原始 HTTP body 字节)。必须使用原始 body,不能 JSON 解析后重新序列化。比较使用恒定时间比较,拒绝与服务器时间相差超过 5 分钟的请求。每次重试刷新签名时间,event.id、occurredAt、body 不变。 参考项目中的 examples/webhook-receiver.py:Python 标准库实现验签、时间窗检查、SQLite 事务落库与去重。示例只接收事件;你的业务 worker 应再处理 inbox 中的 PENDING 记录。 ## 消息结构 ```json { "apiVersion": "1.0", "event": { "id": "稳定的业务事件 ID", "type": "NON_RENEWING_PURCHASE", "projectId": "项目 ID", "appKey": "应用 Key", "appUserId": "业务用户 ID", "customerId": "涨米客户 ID", "environment": "SANDBOX", "occurredAt": "2026-09-07T12:00:00Z", "data": { "orderId": "订单 ID", "originalOrderId": "原始订单 ID", "productId": "week", "productType": "non_renewing", "billingPeriod": "PT2M", "status": "ACTIVE", "purchasedAt": "2026-09-07T12:00:00Z", "expiresAt": "2026-09-07T12:02:00Z", "autoRenewStatus": "NOT_APPLICABLE", "willRenew": null, "priceMicros": 12800000, "priceCurrency": "CNY", "offerKind": "STANDARD" } } } ``` data 还包含订单的应用、客户、eventTime、autoRenewObservedAt、priceCountry、purchaseDuration、durationTypeCode、offerTypeCode、offerId。价格为百万分之一货币单位;未取得金额可能为空,不应视为免费。expiresAt 为空表示该订单没有期限,不能推断所有权益永久有效。客户端不得决定 environment;使用服务端验证结果。接收端允许未知字段以便兼容新增信息。 ## 当前事件 | type | 含义 | |---|---| | INITIAL_PURCHASE | 首次观察到自动续期订阅订单,原始订单相同或未提供 | | RENEWAL | 首次观察到新的自动续期订单,originalOrderId 与本次 orderId 不同 | | CANCELLATION | 同一自动续期订单的续订开关从开启变为关闭,不代表立即失去权益 | | UNCANCELLATION | 自动续订从关闭变为开启 | | SUBSCRIPTION_UPDATED | 自动续期订单的其他已验证状态变化 | | NON_RENEWING_PURCHASE | 非自动续期购买,包含非续期订阅、消耗型和非消耗型 | | EXPIRATION | 已验证过期,或非续期订阅到达服务端确定的期限 | | BILLING_ISSUE | 商店报告扣款重试;不等于过期 | | REFUND | 商店验证为退款或撤销 | | TEST | 后台主动发送的连通性测试,不处理会员 | 非续期到期按原订单 expiresAt 触发,使用购买时确定的生产/沙盒周期。不会按当前商品配置重算,也不会因恢复购买重复生成。仅在存在符合应用、环境且已启用、创建时间不晚于到期时间的接收地址时生成到期业务事件;创建地址前已过期的订单不补造通知。停用期间到期的事件可在重新启用后补发。自动续期依赖华为验证结果,不自行跳过宽限期判定。 事件描述一笔订单的状态,不等于客户所有权益的最终状态。退款旧订单、单笔到期时,客户可能还有其他有效订单。建议业务 worker 用服务端授权的管理接口 GET /v1/admin/projects/{projectId}/customers/{customerId}?environment=NORMAL(或 SANDBOX)重新获取 customerInfo,以当前有效权益更新会员,不按旧事件直接覆盖。管理 Token 只保存在你的业务服务器,禁止放进客户端。不要使用带客户端 SDK 公钥的接口作为业务管理授权。 ## 参考与差异 参考 RevenueCat 官方 Webhook 配置、重试、签名、幂等及最新客户信息同步建议: https://www.revenuecat.com/docs/integrations/webhooks https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields 本接口是涨米自身协议,不宣称与 RevenueCat wire format 完全兼容。涨米使用 camelCase、NORMAL 生产环境、独立 REFUND / SUBSCRIPTION_UPDATED 类型;RevenueCat 用 CANCELLATION 等字段表示退款。RevenueCat 开源 SDK 用于核对客户端身份和恢复购买,服务端投递实现没有公开源码,不能声称复刻其私有实现。 华为订单验证与权益发放原则参照: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/iap-delivering-nonrenewable 华为商店通知配置: https://developer.huawei.com/consumer/cn/doc/doccenter-distribution/parameters-0000001931995692