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",
    "previous_status": "",
    "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 相等就跳过」的合并器,

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

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会员内部 id,不是申请单号GET /v1/cards 列该会员的卡
card会员内部 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

会员建档成功

何时发:POST /v1/members 真的新建了一行会员(返回 201)那一刻。 幂等命中(同一个 external_member_id 已存在,返回 200)不发 —— 否则你每次重试注册都会收到一条「新会员」。

{
  "event_id": "evt_2f1c8a9b4d7e4c1fa0b3e5d6c7a8b901",
  "event_type": "member.created",
  "created_at": "2026-08-12T09:30:00Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "member",
    "id": "4b7c1e02-9a3d-4f18-8c55-2d61ab0f9e77",
    "external_member_id": "u_88123",
    "status": "normal",
    "previous_status": "",
    "status_version": 0
  }
}

member.suspended

商户级停用状态变化(停用与恢复共用这一条)

何时发:POST /v1/members/{id}/suspend 真的改动了 merchant_status 之后。 恢复也发这一条,带 status: normal —— 方向靠 status 区分。 给恢复另造一条事件会让「这个人现在能不能用」需要看两条事件流才答得出。 这一位只影响该会员在你这边的可用性,平台级封禁是另一套、你看不到。

{
  "event_id": "evt_7c93a1d05e6b4a2f9d81c4e0f2a37b56",
  "event_type": "member.suspended",
  "created_at": "2026-08-12T10:02:11Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "member",
    "id": "4b7c1e02-9a3d-4f18-8c55-2d61ab0f9e77",
    "external_member_id": "u_88123",
    "status": "suspended",
    "previous_status": "",
    "status_version": 0
  }
}

kyc.result.updated

实名审核有结果,或被要求补充材料

何时发:两件事共用这一条:审核出结论(status: updated)、被要求补件 (status: supplement_required)。 ⚠ 通过与驳回是同一个 status: updated —— 事件体里没有任何字段能区分, 这是「只带 ID 与状态」那条红线的直接后果。收到它必须回查 GET /v1/kyc(带 x-on-behalf-of)才知道结论;照 status 直接放行是错的。 ⚠ data.id会员内部 id,不是 KYC 单号。 L1 与 L2 也不分事件,回查时看返回里的层级。

{
  "event_id": "evt_1a55e9c73b0d47f2ab6c8d19e4f05237",
  "event_type": "kyc.result.updated",
  "created_at": "2026-08-12T11:15:40Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "kyc",
    "id": "4b7c1e02-9a3d-4f18-8c55-2d61ab0f9e77",
    "external_member_id": "u_88123",
    "status": "updated",
    "previous_status": "",
    "status_version": 0
  }
}

deposit.credited

链上入金已确认并记进会员可用余额

何时发:Cobo 确认到账、入账凭证过账成功之后(不是收到链上通知那一刻)。 收到它时余额已经能查到了。 ⚠ 只覆盖链上入金。你用 POST /v1/deposits 上报的入金不发这条事件 —— 那条接口是同步的,201 返回时钱已经记上了,再发一条事件只是回声。 ⚠ status_version 恒为 0(入金没有状态机,只有「已入账」这一态)。 ⚠ data.id 是我方入金流水号(纯数字),与 GET /v1/deposits 列表里的 dep_<你的 reference> 不是同一套标识

{
  "event_id": "evt_9d2b6f4013ae4c78b5910c3e7d84a2f1",
  "event_type": "deposit.credited",
  "created_at": "2026-08-12T12:44:03Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "deposit",
    "id": "184203",
    "external_member_id": "u_88123",
    "status": "credited",
    "previous_status": "",
    "status_version": 0
  }
}

withdrawal.order.locked

会员自助链上提现已提交,资金已从可用余额扣走

何时发:会员在 App 里提交提现的那一刻 —— 钱这时已经不在可用余额里了 (进了独立的提现在途桶),还没有出款。 把它当成「余额已减少」的通知,不是「钱已经到对方地址」。 终局看 withdrawal.order.completed / .failed。 与 POST /v1/withdrawals 那条两段式扣账无关,见页首说明。

{
  "event_id": "evt_5e81c0a7f39b4d26ae4713b8c0d5f9a2",
  "event_type": "withdrawal.order.locked",
  "created_at": "2026-08-12T13:05:22Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "withdrawal",
    "id": "8f2a41d6-0c7b-4e39-9a15-63d0c8be7f24",
    "external_member_id": "u_88123",
    "status": "locked",
    "previous_status": "",
    "status_version": 1
  }
}

withdrawal.order.completed

会员自助链上提现已出款完成

何时发:上游确认出款完成、结算凭证落账那一刻。这是终态。 中间态(审核通过、已广播上链)刻意不外发 —— 它们是我方内部流程的 可见性,多发只会让你以为有什么要处理。

{
  "event_id": "evt_c47d2be9105f43a8b6e0972d15c8340a",
  "event_type": "withdrawal.order.completed",
  "created_at": "2026-08-12T13:41:57Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "withdrawal",
    "id": "8f2a41d6-0c7b-4e39-9a15-63d0c8be7f24",
    "external_member_id": "u_88123",
    "status": "completed",
    "previous_status": "",
    "status_version": 4
  }
}

withdrawal.order.failed

会员自助链上提现未完成,资金已全额退回可用余额

何时发:两种情形共用这一条:我方审核驳回,或上游出款失败。 两者对会员的下一步完全不同(前者要问客服,后者可以直接重发), 但事件体里区分不了 —— 要知道是哪一种得回查。 手续费在失败时全额退还,不会留在我方。

{
  "event_id": "evt_0b6e3f9d82c74a15ae37d0561fb92c48",
  "event_type": "withdrawal.order.failed",
  "created_at": "2026-08-12T13:52:10Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "withdrawal",
    "id": "8f2a41d6-0c7b-4e39-9a15-63d0c8be7f24",
    "external_member_id": "u_88123",
    "status": "failed",
    "previous_status": "",
    "status_version": 3
  }
}

remittance.order.completed

汇款已到账收款人

何时发:上游回报该笔 payout 完成、我方结算凭证落账那一刻。这是终态。

{
  "event_id": "evt_3ad9165e7b0c4f82a91d64c05e73b8f1",
  "event_type": "remittance.order.completed",
  "created_at": "2026-08-12T14:20:08Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "remittance",
    "id": "1c0e8a37-45bd-4f6a-b2e1-90a7d3f5c862",
    "external_member_id": "u_88123",
    "status": "completed",
    "previous_status": "",
    "status_version": 6
  }
}

remittance.order.failed

汇款未能完成,冻结资金已全额退回

何时发:上游拒付、合规拒绝、或建收款人失败等导致这一单走不下去。 钱已经回到会员可用余额了 —— 你的客服话术要说清这一点, 只说「失败」会立刻换来一通「那我的钱呢」。

{
  "event_id": "evt_86f4c1097d2b4e35a8c0136be59d7f20",
  "event_type": "remittance.order.failed",
  "created_at": "2026-08-12T14:31:44Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "remittance",
    "id": "1c0e8a37-45bd-4f6a-b2e1-90a7d3f5c862",
    "external_member_id": "u_88123",
    "status": "failed",
    "previous_status": "",
    "status_version": 5
  }
}

remittance.order.refunded

汇款到账后被收款行退回,款项已退回会员余额

何时发:与 failed 刻意分开:这一单曾经发过 completed,你的用户看到过 「已到账」。合并成 failed 会让他以为之前那条是误报。

{
  "event_id": "evt_e29a704c6b3d418f95720ad3c8f16b54",
  "event_type": "remittance.order.refunded",
  "created_at": "2026-08-13T09:02:31Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "remittance",
    "id": "1c0e8a37-45bd-4f6a-b2e1-90a7d3f5c862",
    "external_member_id": "u_88123",
    "status": "refunded",
    "previous_status": "",
    "status_version": 8
  }
}

remittance.order.action_required

球在会员脚下:要确认新价格,或要补充材料

何时发:两件事共用这一条:汇率变动超过会员设的上限需要重新确认, 或上游要求补充材料两件都有时限:重新确认逾期这一单自动取消并退回冻结资金; 材料不交这一单就停在那里,钱一直冻着。 收到它请立刻把会员引到这一单上,并回查 GET /v1/remittances/rmt_<id>/pending-action 看究竟要他做什么 —— 事件体里区分不了这两件事。

{
  "event_id": "evt_74b0d3e18f6a4c29ba51e07d962f4c83",
  "event_type": "remittance.order.action_required",
  "created_at": "2026-08-12T14:12:19Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "remittance",
    "id": "1c0e8a37-45bd-4f6a-b2e1-90a7d3f5c862",
    "external_member_id": "u_88123",
    "status": "action_required",
    "previous_status": "",
    "status_version": 3
  }
}

qrpay.order.completed

扫码付成功,上游已向收单商户放行

何时发:上游确认该笔付款完成那一刻。这是终态。

{
  "event_id": "evt_bd15c8073e4a49f2861b09df7ac35e10",
  "event_type": "qrpay.order.completed",
  "created_at": "2026-08-12T15:07:52Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "qrpay_order",
    "id": "6d3f9b21-8c07-4a5e-91d4-27e0b6a3fc58",
    "external_member_id": "u_88123",
    "status": "completed",
    "previous_status": "",
    "status_version": 4
  }
}

qrpay.order.failed

扫码付未成功,扣款已全额退回可用余额

何时发:上游拒付、走廊未开通、报价过期都会走到这里 —— 失败不是罕见路径。 没有这条事件时你唯一的知情方式是轮询,而轮询要先知道有单可查: 你等到的会是一条永远不来的 completed。

{
  "event_id": "evt_af02e71d94b6435c8071cd23e6b95f4a",
  "event_type": "qrpay.order.failed",
  "created_at": "2026-08-12T15:09:03Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "qrpay_order",
    "id": "6d3f9b21-8c07-4a5e-91d4-27e0b6a3fc58",
    "external_member_id": "u_88123",
    "status": "failed",
    "previous_status": "",
    "status_version": 3
  }
}

qrpay.order.refunded

扫码付被退款,款项已退回会员可用余额

何时发:收单侧退款到达之后。它直接影响会员余额,所以与 failed 分开一条 —— 两者在你账上的含义不同(一个是这笔从没成过,一个是成了又退)。

{
  "event_id": "evt_5c8b0a2f61d7452e93af6b04d18e37c9",
  "event_type": "qrpay.order.refunded",
  "created_at": "2026-08-13T10:21:35Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "qrpay_order",
    "id": "6d3f9b21-8c07-4a5e-91d4-27e0b6a3fc58",
    "external_member_id": "u_88123",
    "status": "refunded",
    "previous_status": "",
    "status_version": 6
  }
}

card.application.approved

开卡成功,卡片已可用

何时发:发卡机构开卡成功、卡片落库之后。 ⚠ data.id 是会员内部 id,不是申请单号也不是卡 id。 要拿到卡请用 GET /v1/cardsx-on-behalf-of: <external_member_id>。 ⚠ 失败那一侧(card.application.rejected目前没有生产者, 开卡失败请轮询 GET /v1/cards/applications/cap_<id>

{
  "event_id": "evt_2e6c98a04f1b47d3850ac7e195b3d602",
  "event_type": "card.application.approved",
  "created_at": "2026-08-12T16:03:27Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "card_application",
    "id": "4b7c1e02-9a3d-4f18-8c55-2d61ab0f9e77",
    "external_member_id": "u_88123",
    "status": "approved",
    "previous_status": "",
    "status_version": 0
  }
}

card.status.updated

卡片状态发生变化(冻结 / 解冻 / 激活 / 销卡 / 制卡与物流推进)

何时发:上游回报的卡片状态与我方记录不一致、我方把它同步过来那一刻。 一条事件覆盖全部状态变化,具体变成了什么事件体里没有 —— 回查 GET /v1/cards/crd_<卡 id>status。 ⚠ data.id 是会员内部 id,不是卡 id,所以你得先 GET /v1/cards 列出这个人的卡再逐张比对。 ⚠ 冻结与解冻各有一个过渡态(freezing / unfreezing),它们不是终态 —— 过渡态期间往卡里充值会失败。别看到一条事件就认定卡已经可用。

{
  "event_id": "evt_ca730f5b8e214d6790ab3c1e57f4d028",
  "event_type": "card.status.updated",
  "created_at": "2026-08-12T16:40:11Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "card",
    "id": "4b7c1e02-9a3d-4f18-8c55-2d61ab0f9e77",
    "external_member_id": "u_88123",
    "status": "updated",
    "previous_status": "",
    "status_version": 0
  }
}

card.topup.credited

充值到卡的钱已经真的到卡上了

何时发:上游 webhook 确认到账那一刻,不是下单成功那一刻。 POST /v1/cards/{id}/topups 返回 201 只表示「下单收到了」,钱到卡是 第二段异步。限额与可用额度要等这条事件之后再放开 —— 提前放开等于限额已开而钱没到。

{
  "event_id": "evt_71fd0c9e46a34b18b52d7f038ea6c145",
  "event_type": "card.topup.credited",
  "created_at": "2026-08-12T17:12:48Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "card_topup",
    "id": "3a90c7e4-1f68-4b02-95dc-7e41b0a2d963",
    "external_member_id": "u_88123",
    "status": "credited",
    "previous_status": "",
    "status_version": 2
  }
}

earn.order.settled

理财收益已入账(活期派息 / 定期到期结本息)

何时发:两件不同的事共用这一条: · 活期派息——每期派息过账之后,data.id会员内部 idstatus_version 恒为 0(活期是余额包,不是一笔一单); · 定期到期结算——本息结清之后,data.id 是定期订单号、带 status_version。 区分方法是拿 data.idGET /v1/earn/orders 找:找得到就是定期, 找不到就是活期派息,这时用 GET /v1/earn/positions 看活期本金。 ⚠ 定期起息不发事件 —— 申购那一刻发起方是你(或会员自己),没有新信息。

{
  "event_id": "evt_9e04b7c2513f486da8021c6e5f7b3d90",
  "event_type": "earn.order.settled",
  "created_at": "2026-08-13T00:05:00Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "earn_order",
    "id": "b52c8e17-3d94-40af-8617-c9e0d21a4f35",
    "external_member_id": "u_88123",
    "status": "settled",
    "previous_status": "",
    "status_version": 2
  }
}

exchange.order.executed

站内兑换已成交

何时发:兑换是原子的、不可逆的、没有中间态 —— 只有成交这一条事件。 POST /v1/exchange/orders 是同步接口,返回 201 时已经成交,所以这条事件 主要用在「会员在 App 里自己换的」那一侧,你的服务端据它同步余额。 ⚠ status_version 恒为 0(没有状态机可言)。

{
  "event_id": "evt_4f8d21b60a9e47c5b370ed81c2a6594f",
  "event_type": "exchange.order.executed",
  "created_at": "2026-08-12T18:02:33Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "exchange_order",
    "id": "7e13a5c8-62f0-4d91-ae35-0b8c4d7f2916",
    "external_member_id": "u_88123",
    "status": "executed",
    "previous_status": "",
    "status_version": 0
  }
}

transfer.completed

会员收到一笔站内转账

何时发:转账成交那一刻(一步到账,没有待审态,成交即对方的可用余额)。 ⚠ 只发给收款方。 data.external_member_id收款人的外部号, 发起方一条都不发 —— 他是自己按的确认键,流水里已经有了。 ⚠ 跨商户转账是禁止的,所以收款人一定也是你的会员。 ⚠ status_version 恒为 0。

{
  "event_id": "evt_d6710b3fa825469c8e14f0d539b7a2c1",
  "event_type": "transfer.completed",
  "created_at": "2026-08-12T18:30:07Z",
  "merchant_id": "acme",
  "livemode": true,
  "data": {
    "object": "internal_transfer",
    "id": "0af62d81-59b3-4c07-9e2a-816d3f0b5ca4",
    "external_member_id": "u_88999",
    "status": "completed",
    "previous_status": "",
    "status_version": 0
  }
}