Skip to main content

Webhooks Overview

Overview

Platform sends async notifications (webhooks) to merchant's configured URL when key events occur. All webhook notifications follow a unified three-part structure.


Notification Structure

{
// Part 1: Common fields
"notifyId": "NOTIFY202601070001",
"notifyType": "AGREEMENT_STATUS",
"notifyTime": "2026-01-07T10:30:05+08:00",
"merchantId": "M123456789",

// Part 2: Business data
"data": {
"eventType": "SIGNED",
// Other business fields (varies by notifyType and eventType)
}

// Note: Signature parameters (sign, signType, timestamp, nonce) are in HTTP Headers, not in request body
}

Common Fields

ParameterTypeDescription
notifyIdstringNotification unique ID (for deduplication)
notifyTypestringNotification type category
notifyTimestringNotification time (ISO8601 format: yyyy-MM-dd'T'HH:mm:ssXXX)
merchantIdstringMerchant ID
dataobjectBusiness data
Signature Location

Signature parameters (X-Signature, X-Sign-Type, X-Timestamp, X-Nonce) are provided in HTTP request headers, not in the request body. See Request Headers section below.

Notification Type Categories

notifyTypeDescriptionApplicable Scenarios
AGREEMENT_STATUSAgreement status notificationSign, unsign, suspend, resume and all agreement status changes
TRANSACTION_RESULTTransaction result notificationDeduction, refund and all transaction results
AGREEMENT_TIMEOUTSign timeout notificationSign link/QR code expires without completion
ORDER_TIMEOUTOrder timeout notificationDeduction order times out without completion

Description:

  • For AGREEMENT_STATUS and TRANSACTION_RESULT, specific event type is distinguished by data.eventType field
  • For AGREEMENT_TIMEOUT and ORDER_TIMEOUT, they are standalone notification types without eventType subdivision
  • Specific result status is distinguished by data.status field

Event Types (Agreement)

eventTypeDescriptionCorresponding notifyType
SIGNEDSign eventAGREEMENT_STATUS
UNSIGNEDUnsign eventAGREEMENT_STATUS
SUSPENDEDSuspend eventAGREEMENT_STATUS
TIMEOUTTimeout eventAGREEMENT_STATUS
note

The TIMEOUT eventType is used when agreement enters timeout status. This is different from AGREEMENT_TIMEOUT notifyType which is for sign link/QR code expiration.

Event Types (Transaction)

eventTypeDescriptionCorresponding notifyType
PAYDeduction eventTRANSACTION_RESULT
REFUNDRefund eventTRANSACTION_RESULT

Webhook Handling

Request Headers

ParameterTypeDescription
X-TimestampstringNotification timestamp (milliseconds), used for signature generation and replay attack prevention
X-SignaturestringRSA2 signature value (Base64 encoded), regenerated for each send/retry
X-NoncestringRandom number (5-digit number, range 10000-99999), regenerated for each send/retry
X-Sign-TypestringSignature algorithm, fixed as RSA2
Content-Typestringapplication/json

Signature Description:

  • Signature content: timestamp + nonce + requestBody (string concatenation, not JSON format)
  • Signature algorithm: RSA2 (SHA256withRSA)
  • Request body remains pure JSON format, does not contain signature fields

Retry Mechanism

ConfigurationValue
Retry countMaximum 5 retries
Retry interval15s, 30s, 1min, 5min, 30min
Success indicatorHTTP 200 with body success
Timeout10 seconds per request

Merchant Response

Return plain text success (case insensitive) with HTTP 200:

success

Handling Code Example (Java)

@RestController
@RequestMapping("/webhook")
public class WebhookController {

@PostMapping("/agreement")
public ResponseEntity<String> handleWebhook(
@RequestHeader("X-Timestamp") String timestamp,
@RequestHeader("X-Signature") String signature,
@RequestHeader("X-Nonce") String nonce,
@RequestHeader("X-Sign-Type") String signType,
@RequestBody String body) {

// 1. Verify timestamp (prevent replay attack)
long ts = Long.parseLong(timestamp);
if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
log.warn("Webhook timestamp expired");
return ResponseEntity.status(400).body("timestamp_expired");
}

// 2. Verify signature (signature content = timestamp + nonce + requestBody)
String contentToVerify = timestamp + nonce + body;
if (!verifySignature(contentToVerify, signature)) {
log.warn("Webhook signature verification failed");
return ResponseEntity.status(400).body("signature_error");
}

// 3. Parse notification content (request body is pure JSON, no sign/signType fields)
JSONObject notify = JSON.parseObject(body);
String notifyId = notify.getString("notifyId");
String notifyType = notify.getString("notifyType");
JSONObject data = notify.getJSONObject("data");

// 4. Idempotency check
if (isProcessed(notifyId)) {
return ResponseEntity.ok("success");
}

// 5. Process business based on notification type
try {
switch (notifyType) {
case "AGREEMENT_STATUS":
handleAgreementStatusNotify(data);
break;
case "TRANSACTION_RESULT":
handleTransactionResultNotify(data);
break;
case "AGREEMENT_TIMEOUT":
handleAgreementTimeoutNotify(data);
break;
case "ORDER_TIMEOUT":
handleOrderTimeoutNotify(data);
break;
default:
log.warn("Unknown notification type: {}", notifyType);
}

markAsProcessed(notifyId);
return ResponseEntity.ok("success");
} catch (Exception e) {
log.error("Error processing webhook", e);
return ResponseEntity.status(500).body("processing_error");
}
}
}

Notes

  1. Deduplication: Use notifyId to prevent duplicate processing
  2. Signature Verification: Always verify signature before processing
  3. Idempotency: Same notification may be sent multiple times; ensure idempotent handling
  4. Response Quickly: Return success immediately; process business asynchronously if needed
  5. Event Type Check: Use eventType and orderType to distinguish specific events within each notification category