Skip to content

Async Callback

Updated: 9/11/26, 8:21:59 PM

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:

transportDeliveryACK semanticsRetry
httpHTTP request (POST JSON or GET query, per format)HTTP 2xx and body trimmed equals SUCCESS16 increasing retries
mqPublish to Artemis Topic daxpay.notice.<appId>publish success = delivered3 short retries

Format (format) — how the payload is assembled and signed:

FormatApplies toMethodPayloadVerification
systemOrders from standard Payment API (/unipay/*)POSTJSON bodyRSA (SHA256withRSA)
easy_payOrders from EasyPay plugin (/epay/*)GETURL query paramsMD5 / 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)

  1. Merchant provides notifyUrl in the payment request.
  2. Platform sends a POST request (JSON) to notifyUrl.
  3. Merchant verifies signature first, then processes business logic.
  4. Merchant returns SUCCESS (case-insensitive; leading/trailing whitespace is trimmed).
  5. 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 notifyUrl from the payment request, always http + 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 #IntervalCumulative
115s15s
215s30s
330s1min
43min4min
510min14min
620min34min
7~930min × 3~2h4min
1060min~3h4min
11~133h × 3~12h4min
14~166h × 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 SUCCESS case-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:

java
@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:

FieldTypeDescription
eventstringNotification event code (see below)
protocolstringNotification protocol (system / easy_pay)
mchNostringMerchant number
appIdstringApplication ID
codeintStatus code (0 = success)
msgstringMessage
dataobjectBusiness data (order/refund snapshot)
signstringPlatform RSA signature (Base64)
resTimestringNotification time (Beijing time, yyyy-MM-dd HH:mm:ss)
reqIdstringRequest ID

Event Types (NoticeEventEnum)

eventDescription
pay.successPayment successful
pay.failPayment failed
pay.closePayment closed (manual close)
pay.timeoutPayment timeout closed (order expired unpaid)
pay.cancelPayment cancelled (fund status CANCEL)
refund.successRefund successful
refund.failRefund failed
refund.closeRefund closed
transfer.successTransfer successful
transfer.failTransfer failed
transfer.closeTransfer closed
alloc.successAllocation successful
alloc.failAllocation failed
risk.hitRisk rule hit (blacklist / overseas IP, etc.)

Transfer and allocation notifications use the notify address carried by the corresponding business order (the notifyUrl passed 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

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

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

  1. Get all fields from the callback JSON.
  2. Remove sign.
  3. Exclude empty values.
  4. Sort by key in ASCII order.
  5. Join as key1=value1&key2=value2.
  6. 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)
text
SUCCESS

Non-SUCCESS body, non-2xx HTTP, or timeout (15s) triggers a retry per the Retry Strategy above.

Official Website · Released under the GNU LGPL v3.0