Async Callback
Overview
When order status changes (payment success, failure, close, refund success), DaxPay sends async notifications to the merchant. "How it is delivered" and "what the payload looks like" are two orthogonal dimensions: transport (HTTP callback / MQ push) × format (system / easy_pay).
Transport and Format
Transport (transport) — how the notification is delivered:
| transport | Delivery | ACK semantics | Retry |
|---|---|---|---|
http | HTTP request (POST JSON or GET query, per format) | HTTP 2xx and body trimmed equals SUCCESS | 16 increasing retries |
mq | Publish to Artemis Topic daxpay.notice.<appId> | publish success = delivered | 3 short retries |
Format (format) — how the payload is assembled and signed:
| Format | Applies to | Method | Payload | Verification |
|---|---|---|---|---|
system | Orders from standard Payment API (/unipay/*) | POST | JSON body | RSA (SHA256withRSA) |
easy_pay | Orders from EasyPay plugin (/epay/*) | GET | URL query params | MD5 / RSA (depends on V1/V2) |
easy_pay uses GET
The EasyPay-compatible format (format=easy_pay) sends callbacks via GET + URL query params, not POST JSON. When integrating EasyPay, read params via request.getParameter(), not request.getReader().
Notification Flow (system protocol)
- Merchant provides
notifyUrlin the payment request. - Platform sends a POST request (JSON) to
notifyUrl. - Merchant verifies signature first, then processes business logic.
- Merchant returns
SUCCESS(case-insensitive; leading/trailing whitespace is trimmed). - If response is not
SUCCESS, HTTP non-2xx, or timeout, the system retries.
App-level Subscription (Parallel Notification)
Besides the order-level notifyUrl (passed per order), merchants can configure a unified notification (HTTP callback URL or MQ push) + subscribed events for an app in "Async Notify Config". The same business event parallelly generates two independent notification tasks:
- Order-level (source=order): uses the
notifyUrlfrom the payment request, alwayshttp + system - App-level (source=app): uses the app config; transport is decided by
notifyWay(http/mq)
You may receive two notifications
If a merchant both passes a notifyUrl in the order and configures an app-level subscription, the same event is sent separately to the order-level URL and the app-level address (two independent notifications, each retried independently). This is by design (dual-track parallel), useful for pushing to both reconciliation and business systems. Only a single path is used when the order-level URL is empty / the app-level config is absent.
App-level config is available in the admin/merchant panel under "App Management → Async Notify Config".
Retry Strategy
Fixed 16 retries with increasing intervals (implemented in NoticeRetryPolicy, scheduled via Artemis delayed queue):
| Retry # | Interval | Cumulative |
|---|---|---|
| 1 | 15s | 15s |
| 2 | 15s | 30s |
| 3 | 30s | 1min |
| 4 | 3min | 4min |
| 5 | 10min | 14min |
| 6 | 20min | 34min |
| 7~9 | 30min × 3 | ~2h4min |
| 10 | 60min | ~3h4min |
| 11~13 | 3h × 3 | ~12h4min |
| 14~16 | 6h × 3 | ~30h4min |
- Max retries: 16 (after which sending stops and the task is marked failed)
- Only auto-send path retries: Manual "resend notification" from the merchant panel does not auto-retry on failure — must be triggered again manually
- ACK logic: HTTP 2xx and body trimmed equals
SUCCESScase-insensitively; either missing triggers a retry
Retry for MQ push
For transport=mq, only a failed publish to the Topic (broker failure / network error) is retried 3 times at 10s / 30s / 60s; consumption-side failures are the merchant's/MQ's responsibility (use a JMS durable subscriber so offline messages aren't lost).
MQ Push
When an app's "Async Notify Config" selects notifyWay=mq, the platform publishes the notification (a system-format DaxNoticeResult JSON, with RSA signature) to an app-isolated Topic for the merchant to consume.
- Topic name:
daxpay.notice.<appId>, isolated per app - Payload: the same DaxNoticeResult JSON as an HTTP callback; verification is identical
- ACK semantics: publish success = delivered (aligns with the Stripe EventBridge model)
- Broker requirement: the Artemis address must be configured as multicast routing type; use a durable subscriber on the merchant side
Java JMS consumer example:
@JmsListener(
destination = "daxpay.notice.APP001", // matches the appId
containerFactory = "topicListenerFactory", // pubSubDomain=true Topic factory
subscription = "daxpay-notify-app001" // durable subscription name (dedup across instances of the same appId)
)
public void onNotice(String body) {
// body is a DaxNoticeResult JSON; verification is identical to HTTP callback
// see "Signature Verification" below
}Callback Message Format
Method: POST Content-Type: application/json
Structure (DaxNoticeResult)
Extends DaxResult with event and merchant fields:
| Field | Type | Description |
|---|---|---|
| event | string | Notification event code (see below) |
| protocol | string | Notification protocol (system / easy_pay) |
| mchNo | string | Merchant number |
| appId | string | Application ID |
| code | int | Status code (0 = success) |
| msg | string | Message |
| data | object | Business data (order/refund snapshot) |
| sign | string | Platform RSA signature (Base64) |
| resTime | string | Notification time (Beijing time, yyyy-MM-dd HH:mm:ss) |
| reqId | string | Request ID |
Event Types (NoticeEventEnum)
| event | Description |
|---|---|
| pay.success | Payment successful |
| pay.fail | Payment failed |
| pay.close | Payment closed (manual close) |
| pay.timeout | Payment timeout closed (order expired unpaid) |
| pay.cancel | Payment cancelled (fund status CANCEL) |
| refund.success | Refund successful |
| refund.fail | Refund failed |
| refund.close | Refund closed |
| transfer.success | Transfer successful |
| transfer.fail | Transfer failed |
| transfer.close | Transfer closed |
| alloc.success | Allocation successful |
| alloc.fail | Allocation failed |
| risk.hit | Risk rule hit (blacklist / overseas IP, etc.) |
Transfer and allocation notifications use the notify address carried by the corresponding business order (the
notifyUrlpassed in the allocation request) and also support app-level subscription; risk events (risk.hit) are received via app-level subscription. Allocation notification snapshots include the detail list.
Example: Payment Success
{
"event": "pay.success",
"protocol": "system",
"mchNo": "M200000001",
"appId": "APP001",
"code": 0,
"msg": "success",
"data": {
"orderNo": "P2024120112345700001",
"bizOrderNo": "ORDER20241201001",
"tradeNo": "T2024120112345700001",
"amount": 100,
"realAmount": 100,
"status": "success",
"method": "wechat_qr",
"payTime": "2024-12-01 12:00:00",
"attach": "{\"orderId\": 123}"
},
"sign": "Base64Signature",
"resTime": "2024-12-01 12:00:00",
"reqId": "NTF20241201001"
}Example: Refund Success
{
"event": "refund.success",
"protocol": "system",
"mchNo": "M200000001",
"appId": "APP001",
"code": 0,
"msg": "success",
"data": {
"refundNo": "R2024120112345700001",
"bizRefundNo": "REFUND20241201001",
"bizOrderNo": "ORDER20241201001",
"amount": 100,
"status": "success",
"finishTime": "2024-12-01 14:00:00"
},
"sign": "Base64Signature",
"resTime": "2024-12-01 14:00:00",
"reqId": "NTF20241201002"
}Signature Verification
Merchant must verify the sign field to ensure the message is from DaxPay and untampered.
Steps
- Get all fields from the callback JSON.
- Remove
sign. - Exclude empty values.
- Sort by key in ASCII order.
- Join as
key1=value1&key2=value2. - Verify with platform public key using
SHA256withRSA.
Verify before processing
Always verify signature before processing business logic to prevent forged notifications.
See Signature for code examples.
Merchant Response
After successful verification and processing, return:
- HTTP status: 2xx (200-299)
- Body:
SUCCESS(case-insensitive; leading/trailing whitespace is trimmed)
SUCCESSNon-SUCCESS body, non-2xx HTTP, or timeout (15s) triggers a retry per the Retry Strategy above.