Z Zise Developers

Webhook 事件

20 条事件。事件体只带 ID 与状态—— 不含金额、不含卡号任何片段、不含风控原因、不含上游名称。要详情请拿 ID 回查。

一条事件长什么样

我方向你配置的端点发 POSTContent-Type: application/json,超时 10 秒。

请求头四个:


Content-Type: application/json
z-signature:  t=<unix秒>,v1=<base64>
z-event-id:   evt_2f1c8a9b4d7e4c1fa0b3e5d6c7a8b901
z-event-type: deposit.credited

请求体的形状对所有事件都一样,只有 data 里的值不同:


{
  "event_id": "evt_2f1c8a9b4d7e4c1fa0b3e5d6c7a8b901",
  "event_type": "deposit.credited",
  "created_at": "2026-08-12T09:30:00Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "deposit",
    "id": "184203",
    "external_member_id": "u_88123",
    "status": "credited",
    "status_version": 0
  }
}

红线:事件体只带 ID 与状态。

没有金额、没有资产代码、没有卡号的任何片段、没有风控原因、没有上游名称。

这不是省字节 —— webhook 端点是你的服务,我方没有办法保证它的传输与存储;

GET /v1/<资源>/{id} 那条路径上有 API Key、scope、代理会员三层校验。

要详情就拿 data.id 回查,别指望从事件体里读出金额来记账。

验签

v1 = HMAC-SHA256(你的 webhook_secret, "<t>." + 原始请求体字节),base64 输出。

签的是原始字节,不是你反序列化再序列化一遍的 JSON —— 后者会因为键序或

空白差异算出另一个值,而那种失败看起来像「我方签错了」。

校验 t 落在 ±300 秒内,否则一份被截获的旧请求可以被无限重放。

幂等与向前合并

我方保证至少一次,不保证恰好一次。同一次状态变更重复到达是常态

(资金线的 webhook 与巡检共用同一段落地代码,这是刻意的)。

1. 按 event_id 幂等。重投(你在商户后台点的,或我方退避重试的)**沿用同一个

event_id** —— 换新 id 会让「重投」在你这边表现成「又发生了一次」,

对资金事件而言那正是一次错误的重复入账。

2. 按 status_version 向前合并,收到倒序的事件直接丢弃。少了它,一次慢响应

会把「已到账」打回「处理中」,而两边都不会报错。

status_version 不是所有事件都有(下面每条事件里注明了)。恒为 0 的那几条

没有版本判据,只能按 created_at 比 —— 别写一个「version 相等就跳过」的合并器,

那会让这几条事件只有第一条生效。

今天恒为 0 的是这几条,每一条都写明了原因(不是待办,是这门业务本来就

没有第二条会乱序的事件):

事件为什么没有版本
member.created / member.suspended会员行上没有状态机版本;停用/恢复是同一位的两个取值
kyc.result.updated对象是不是单,L1/L2 分表、补件连单号都没有
deposit.credited入金对外只有「已入账」一态,一个对象一生只发一条事件
exchange.order.executed兑换原子、同步、不可逆 —— 成交即终局
transfer.completed站内转账一步到账,没有待审态
earn.order.settled仅活期派息那一半活期是余额包不是一笔一单;定期那一半带版本

其余事件都带真实版本号。⚠ **card.application.approvedcard.status.updated

在 2026-08-13 之前也是 0**,且 data.id 发的是会员内部 id —— 那是我方的缺陷,

已修。若你的 handler 里写着「卡这两条的 id 是会员 id」,请改回按 data.object

的常规约定读(申请单号 / 卡 id)。

没有 previous_status(2026-08-13 起从事件体里删掉)。它此前恒为空串:

一个生产者都没有。删而不是补上,因为

① 恒空的字段比没有更坏 —— "" 读起来像「上一个状态是空」,

于是 if (data.previous_status === "processing") 这种前置判断**永远不成立

且不报错**;

② 我方也补不出诚实的值:事件产生时那张单早已改完,而同一次变更重复到达是常态

(webhook 与巡检共用同一段落地代码),第二次到达时「上一个状态」已经等于新状态。

一个错的 previous_status 比没有更危险。

状态机的前置判断请用你自己库里的当前值。

重试与死信

首投失败后退避重试,等待 1 / 5 / 30 / 120 / 360 分钟,最多投递 6 次

(首投 + 5 次重投)。第 6 次仍失败转 dead 并在商户后台告警。

不会静默丢弃,也不会无限重试。

成功判据是任意 2xx,响应体我方不解析。所以先回 2xx 再异步处理,

别在 handler 里同步做重活 —— 超过 10 秒我方按失败处理并进入退避,

而你那边其实已经处理完了。

一个端点一行

扇出发生在入队那一刻:配了三个端点就落三行投递,各有自己的 attempts、

退避与 dead。所以某一个端点挂了不会连累另外两个,也不会出现「有一个通了

就整条标已送达」而另外两个永远收不到。

端点在事件产生之后被你删掉或停用了,那一行标 no_subscriber 而不是重试到

dead —— 死信面是要人处理的告警,「自己关掉的端点」不该出现在那里。

产生事件时一个订阅方都没有,同样落一行 no_subscriber:这样「事件产生了但

没人订阅」与「事件根本没产生」在商户后台上是两种不同的显示。

沙盒与生产

订阅按 env 隔离:沙盒端点收不到生产事件。业务产生的事件一律 livemode: true

—— 今天沙盒与 live 共用同一套账本,事件对应的是真实分录。

data.id 是内部原始 id,不带 REST 接口那层前缀

这是最容易踩的一个坑:GET /v1/remittances/... 返回的 id 是 rmt_<uuid>

而事件里的 data.id裸的 <uuid>。回查时自己拼前缀(多数端点两种都认,

但别赌)。另有几条事件的 data.id 根本不是那张单的号,见下表最后一列。

回查一律要带 x-on-behalf-of,值就用事件里的 external_member_id

data.objectdata.id 是什么拿它回查
member会员内部 idGET /v1/members/mem_<id>
kyc会员内部 id,不是 KYC 单号GET /v1/kyc
deposit我方入金流水号(纯数字)没有单笔端点;GET /v1/deposits 列表里的 dep_<你上报的 reference>另一套标识,两者对不上
withdrawal会员自助链上提现单号开放 API 上没有回查端点(见下)
remittance汇款单号GET /v1/remittances/rmt_<id>
qrpay_order扫码付单号GET /v1/qrpay/orders/qrp_<id>
card_application开卡申请单号GET /v1/cards/applications/cap_<id>
card卡 idGET /v1/cards/crd_<id>
card_topup充值到卡单号没有单笔端点;GET /v1/cards/crd_<卡 id> 看余额
earn_order定期结算时是定期订单号;活期派息时是会员内部 idGET /v1/earn/orders(只覆盖定期那一半)
exchange_order兑换单号GET /v1/exchange/orders 列表(那里的 id 是 exc_<id>
internal_transfer转账单号GET /v1/transfers/itr_<id>

**withdrawal.order.*POST /v1/withdrawals 不是同一门生意**

两者的 id 都长得像 wdr_,但落在两张表上:

我方出款;

的两段式扣账,全程由你驱动,因此不产生任何事件(你已经知道结果了)。

把事件里的 id 拿去 GET /v1/withdrawals/{id} 会得到 not_found

平台自营会员不发事件,只有归属到某个商户的会员才会触发桥接。

订阅清单里有、但今天不会到达的两条

商户后台的订阅勾选框里还留着 card.application.rejected

card.application.submitted。它们当前没有生产者 —— 开卡失败与开卡补件今天

只发邮件,没有落会员通知,而 webhook 的唯一桥接点是会员通知。

所以本页不登记它们:登记了你会写一个 handler 然后一直等。

开卡结果请轮询 GET /v1/cards/applications/cap_<id>,或等

card.application.approved(成功那一侧是有生产者的)。

member.created 会员建档成功 member.suspended 商户级停用状态变化(停用与恢复共用这一条) kyc.result.updated 实名审核有结果,或被要求补充材料 非端点发起 deposit.credited 链上入金已确认并记进会员可用余额 非端点发起 withdrawal.order.locked 会员自助链上提现已提交,资金已从可用余额扣走 非端点发起 withdrawal.order.completed 会员自助链上提现已出款完成 非端点发起 withdrawal.order.failed 会员自助链上提现未完成,资金已全额退回可用余额 非端点发起 remittance.order.completed 汇款已到账收款人 remittance.order.failed 汇款未能完成,冻结资金已全额退回 remittance.order.refunded 汇款到账后被收款行退回,款项已退回会员余额 remittance.order.action_required 球在会员脚下:要确认新价格,或要补充材料 qrpay.order.completed 扫码付成功,上游已向收单商户放行 qrpay.order.failed 扫码付未成功,扣款已全额退回可用余额 qrpay.order.refunded 扫码付被退款,款项已退回会员可用余额 card.application.approved 开卡成功,卡片已可用 card.status.updated 卡片状态发生变化(冻结 / 解冻 / 激活 / 销卡 / 制卡与物流推进) card.topup.credited 充值到卡的钱已经真的到卡上了 earn.order.settled 理财收益已入账(活期派息 / 定期到期结本息) exchange.order.executed 站内兑换已成交 transfer.completed 会员收到一笔站内转账