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
| Parameter | Type | Description |
|---|---|---|
| notifyId | string | Notification unique ID (for deduplication) |
| notifyType | string | Notification type category |
| notifyTime | string | Notification time (ISO8601 format: yyyy-MM-dd'T'HH:mm:ssXXX) |
| merchantId | string | Merchant ID |
| data | object | Business 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
| notifyType | Description | Applicable Scenarios |
|---|---|---|
AGREEMENT_STATUS | Agreement status notification | Sign, unsign, suspend, resume and all agreement status changes |
TRANSACTION_RESULT | Transaction result notification | Deduction, refund and all transaction results |
AGREEMENT_TIMEOUT | Sign timeout notification | Sign link/QR code expires without completion |
ORDER_TIMEOUT | Order timeout notification | Deduction order times out without completion |
Description:
- For
AGREEMENT_STATUSandTRANSACTION_RESULT, specific event type is distinguished bydata.eventTypefield - For
AGREEMENT_TIMEOUTandORDER_TIMEOUT, they are standalone notification types without eventType subdivision - Specific result status is distinguished by
data.statusfield
Event Types (Agreement)
| eventType | Description | Corresponding notifyType |
|---|---|---|
SIGNED | Sign event | AGREEMENT_STATUS |
UNSIGNED | Unsign event | AGREEMENT_STATUS |
SUSPENDED | Suspend event | AGREEMENT_STATUS |
TIMEOUT | Timeout event | AGREEMENT_STATUS |
備註
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)
| eventType | Description | Corresponding notifyType |
|---|---|---|
PAY | Deduction event | TRANSACTION_RESULT |
REFUND | Refund event | TRANSACTION_RESULT |
Webhook Handling
Request Headers
| Parameter | Type | Description |
|---|---|---|
| X-Timestamp | string | Notification timestamp (milliseconds), used for signature generation and replay attack prevention |
| X-Signature | string | RSA2 signature value (Base64 encoded), regenerated for each send/retry |
| X-Nonce | string | Random number (5-digit number, range 10000-99999), regenerated for each send/retry |
| X-Sign-Type | string | Signature algorithm, fixed as RSA2 |
| Content-Type | string | application/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
| Configuration | Value |
|---|---|
| Retry count | Maximum 5 retries |
| Retry interval | 15s, 30s, 1min, 5min, 30min |
| Success indicator | HTTP 200 with body success |
| Timeout | 10 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
- Deduplication: Use
notifyIdto prevent duplicate processing - Signature Verification: Always verify signature before processing
- Idempotency: Same notification may be sent multiple times; ensure idempotent handling
- Response Quickly: Return
successimmediately; process business asynchronously if needed - Event Type Check: Use
eventTypeandorderTypeto distinguish specific events within each notification category