Z Zise Developers 简体中文

Webhook Events

26 events. Payloads contain only IDs and status — no amounts, card number fragments, risk-control reasons or provider names. Retrieve details using the ID.

What an event looks like

We send a POST to your configured endpoint with Content-Type: application/json and a 10-second timeout. There are four request headers:


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

The request body has the same shape for every event; only the values inside data differ:


{
  "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
  }
}

Strict boundary: event bodies contain only IDs and statuses.

No amounts, asset codes, card-number fragments, risk-control reasons, or upstream names. This is not about saving bytes: the webhook endpoint is your service, and we cannot guarantee its transport or storage security. The GET /v1/<资源>/{id} path, by contrast, checks the API key, scope, and represented member. Retrieve details using data.id; do not expect to book amounts directly from event bodies.

Signature verification

v1 = HMAC-SHA256(你的 webhook_secret, "<t>." + 原始请求体字节), with base64 output. The signature covers the raw bytes, not JSON that you deserialize and serialize again. Differences in key ordering or whitespace would produce a different value, which can look like an incorrect signature from us. Verify that t is within ±300 seconds; otherwise, an intercepted old request could be replayed indefinitely.

Idempotency and forward merging

We guarantee at-least-once, not exactly-once, delivery. Duplicate delivery of the same status transition is normal; the webhook and reconciliation paths for financial flows deliberately share the same finalization code.

  1. Deduplicate by event_id. Redelivery, whether requested in your merchant console or caused by our backoff retries, reuses the same event_id. A new ID would make a redelivery appear to be another occurrence, which for a financial event would cause an erroneous duplicate credit.
  2. Merge forward using status_version, discarding out-of-order events. Without this, a slow response could regress “credited” to “processing” without either side reporting an error.

Not every event has a usable status_version; each entry below specifies this. Events with a constant 0 have no version criterion and can only be compared using created_at. Do not skip events whose versions are equal, or only the first event of those types will take effect.

The following currently remain at 0. Each has an explanation: these are not pending fixes; the underlying business objects do not have a second event of the kind that would require order-version sequencing:

EventWhy there is no version
member.created / member.suspendedThe member row has no state-machine version; suspension/restoration are two values of the same flag
kyc.result.updatedThe object is a person, not an order; L1/L2 use separate tables, and supplementary-document requests do not even have an order ID
deposit.creditedThe only public deposit state is “credited”; an object emits only one event in its lifetime
exchange.order.executedExchange is atomic, synchronous, and irreversible: execution is final
earn.order.settled (only the flexible-interest-payment variant)Flexible Earn is a balance pool, not one order per transaction; fixed-term settlement has a version

All other events carry actual version numbers. ⚠ card.application.approved and card.status.updated also had 0 before 2026-08-13, and their data.id contained the internal member ID. This was our defect and is fixed. If your handler assumes those two card events contain a member ID, restore the normal convention based on data.object: application ID / card ID.

There is no previous_status; it was removed from event bodies on 2026-08-13. Previously it was always empty, with no producers. We removed it rather than populating it because: ① An always-empty field is worse than no field: "" suggests that the previous status was empty, so a precondition such as if (data.previous_status === "processing") never matches and produces no error. ② We cannot populate an honest value either: the order has already changed when the event is generated, and duplicate delivery of the same transition is normal because webhooks and reconciliation share finalization code. On the second delivery, the “previous status” already equals the new status. An incorrect previous_status is more dangerous than none. Use your own database's current value for state-machine preconditions.

Retries and dead letters

After an initial failure, retries wait 1 / 5 / 30 / 120 / 360 minutes: at most 6 deliveries (the initial attempt + 5 retries). Failure on attempt 6 moves the delivery to dead and raises an alert in the merchant console. Deliveries are never silently discarded, and retries are not unlimited.

Any 2xx counts as success; we do not parse the response body. Acknowledge with 2xx first, then process asynchronously. Do not perform heavy synchronous work in the handler: after 10 seconds we mark the attempt failed and start backoff, even if your side has actually completed processing.

One row per endpoint

Fanout occurs when the event is queued: three configured endpoints produce three delivery rows, each with its own attempts, backoff, and dead state. One unavailable endpoint does not affect the other two, and one successful endpoint cannot mark the whole event delivered while the other two never receive it.

If you delete or disable an endpoint after event creation, its row becomes no_subscriber rather than retrying until dead. Dead letters require human attention; an endpoint you deliberately disabled should not appear there. If there are no subscribers at event creation, we still record one no_subscriber row, so the merchant console distinguishes “an event was generated but nobody subscribed” from “no event was generated”.

Sandbox and production

The environment follows the business-data entity: sandbox keys generate livemode: false events, and production keys generate livemode: true events. The sandbox entity inherits the main merchant's webhook configuration. Matching sandbox endpoints are preferred; otherwise delivery falls back to the main merchant's live endpoints. Production events are never sent to sandbox endpoints.

data.id follows each resource's established format; do not universally add or remove prefixes

A common trap: GET /v1/remittances/... returns an ID such as rmt_<uuid>, but event data.id contains the bare <uuid>. Add the prefix for retrieval yourself; most endpoints accept both forms, but do not rely on that. Some events' data.id is not the order ID at all; see the final column below.

Always supply x-on-behalf-of when retrieving, using the event's external_member_id.

data.objectWhat data.id containsHow to retrieve it
memberInternal member IDGET /v1/members/mem_<id>
kycInternal member ID, not a KYC application IDGET /v1/kyc
depositOur deposit transaction ID, numeric onlyNo single-item endpoint; dep_<你上报的 reference> in GET /v1/deposits uses a different identifier, so they cannot be matched directly
withdrawalMember-initiated on-chain withdrawal IDNo retrieval endpoint in the Open API; see below
remittanceRemittance IDGET /v1/remittances/rmt_<id>
qrpay_orderQR payment order IDGET /v1/qrpay/orders/qrp_<id>
card_applicationPublic application ID, already prefixed with cap_GET /v1/cards/applications/{data.id}; do not add another prefix
cardCard IDGET /v1/cards/crd_<id>
card_topupCard top-up order IDNo single-item endpoint; check the balance with GET /v1/cards/crd_<卡 id>
card_withdrawalCard withdrawal order ID, a bare UUID; REST uses cwd_<id>No single-item endpoint; check the balance with GET /v1/cards/crd_<卡 id>
earn_orderFixed-term order ID for maturity settlement; internal member ID for flexible interest paymentsGET /v1/earn/orders covers only fixed-term orders
exchange_orderExchange order IDGET /v1/exchange/orders list, where the ID is exc_<id>

**withdrawal.order.* and POST /v1/withdrawals represent different flows**

Both use IDs resembling wdr_, but are stored in different tables:

Using an event's ID with GET /v1/withdrawals/{id} returns not_found.

Platform-direct members do not generate events. Only members belonging to a merchant trigger the bridge.

Distinguish card issuance notifications from cardholder review notifications

card.application.rejected means application execution has failed. card.application.submitted may synchronize review approval for an associated cardholder. When the latter carries cardholder_review_status: approved, only the cardholder has been approved; the card has not necessarily been issued, and the application's own status retains its actual progress. card.application.awaiting_bind is sent when a physical card has reached the user and can be bound (standard downstream-inventory applications are automatically handed over in person after approval; platform-direct fulfillment requires confirmation of an in-person handover or shipment receipt by the user). Platform-direct members do not receive it. For other supplementary-document or in-transit shipping states, retrieve the application details. The absence of a notification does not mean the state has not changed.

member.created Member record created successfully member.suspended Merchant-level suspension state changed; suspension and restoration share this event kyc.result.updated Identity verification has a result or requires supplementary documents Not initiated by an endpoint deposit.credited On-chain deposit confirmed and credited to the member’s available balance Not initiated by an endpoint withdrawal.order.locked Member-initiated on-chain withdrawal submitted; funds removed from the available balance Not initiated by an endpoint withdrawal.order.completed Member-initiated on-chain withdrawal paid out Not initiated by an endpoint withdrawal.order.failed Member-initiated on-chain withdrawal unsuccessful; all funds returned to the available balance Not initiated by an endpoint remittance.order.completed Remittance received by the payee remittance.order.failed Remittance unsuccessful; all locked funds returned remittance.order.refunded Remittance returned by the receiving bank after completion; funds returned to the member balance remittance.order.action_required Member action required: confirm a new price or provide supplementary documents qrpay.order.completed QR payment successful; upstream funds released to the acquiring merchant qrpay.order.failed QR payment unsuccessful; the full debit returned to the available balance qrpay.order.refunded QR payment refunded; funds returned to the member’s available balance card.application.approved Card issued successfully and available for use card.application.rejected Card application execution failed card.application.submitted Review status synchronized for an associated cardholder card.application.awaiting_bind Physical card ready to bind card.3ds.received Card 3DS / OTP verification code received Not initiated by an endpoint card.status.updated Card status changed: freeze / unfreeze / activate / close / manufacturing and shipping progress card.topup.credited Card top-up funds have actually reached the card card.withdraw.completed Card funds returned to the member wallet card.transaction.updated Card transaction received or its status updated Not initiated by an endpoint merchant.balance.waterline Aggregate downstream merchant funding-pool level changed Not initiated by an endpoint earn.order.settled Earn proceeds credited: flexible interest payments / fixed-term principal and interest at maturity exchange.order.executed Internal exchange executed