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.
- Deduplicate by
event_id. Redelivery, whether requested in your merchant console or caused by our backoff retries, reuses the sameevent_id. A new ID would make a redelivery appear to be another occurrence, which for a financial event would cause an erroneous duplicate credit. - 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:
| Event | Why there is no version |
|---|---|
member.created / member.suspended | The member row has no state-machine version; suspension/restoration are two values of the same flag |
kyc.result.updated | The object is a person, not an order; L1/L2 use separate tables, and supplementary-document requests do not even have an order ID |
deposit.credited | The only public deposit state is “credited”; an object emits only one event in its lifetime |
exchange.order.executed | Exchange 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.object | What data.id contains | How to retrieve it |
|---|---|---|
member | Internal member ID | GET /v1/members/mem_<id> |
kyc | Internal member ID, not a KYC application ID | GET /v1/kyc |
deposit | Our deposit transaction ID, numeric only | No single-item endpoint; dep_<你上报的 reference> in GET /v1/deposits uses a different identifier, so they cannot be matched directly |
withdrawal | Member-initiated on-chain withdrawal ID | No retrieval endpoint in the Open API; see below |
remittance | Remittance ID | GET /v1/remittances/rmt_<id> |
qrpay_order | QR payment order ID | GET /v1/qrpay/orders/qrp_<id> |
card_application | Public application ID, already prefixed with cap_ | GET /v1/cards/applications/{data.id}; do not add another prefix |
card | Card ID | GET /v1/cards/crd_<id> |
card_topup | Card top-up order ID | No single-item endpoint; check the balance with GET /v1/cards/crd_<卡 id> |
card_withdrawal | Card withdrawal order ID, a bare UUID; REST uses cwd_<id> | No single-item endpoint; check the balance with GET /v1/cards/crd_<卡 id> |
earn_order | Fixed-term order ID for maturity settlement; internal member ID for flexible interest payments | GET /v1/earn/orders covers only fixed-term orders |
exchange_order | Exchange order ID | GET /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:
withdrawal.order.*events describe on-chain withdrawals initiated by members in the App, reviewed and paid out by us.POST /v1/withdrawals/.../confirm/.../failimplement two-phase debiting when you execute the on-chain payout. You drive the entire flow, so it emits no events: you already know the result.
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