Webhook
Webhook allows you to receive real-time asynchronous notifications from SUNBAY when transaction status changes. Through Webhook, you can update order status promptly and trigger business processes without frequent polling of query interfaces.
Overview
When a transaction status changes to any of the following states, SUNBAY will send a Webhook notification to your configured notifyUrl:
SUCCESS- Transaction successfulFAIL- Transaction failedCLOSED- Transaction closed
Supported transaction types include:
SALE(Sale)AUTH(Pre-authorization)POST_AUTH(Pre-authorization completion)FORCED_AUTH(Forced authorization)INCREMENTAL(Incremental authorization)REFUND(Refund)VOID(Void)
And all other transaction scenarios.
Request Specification
Basic Information
| Property | Description |
|---|---|
| Request Method | POST |
| Request URL | The notifyUrl you provided when initiating the transaction |
| Content-Type | application/json; charset=utf-8 |
| Connection Timeout | 5 seconds |
| Read Timeout | 10 seconds |
Response Requirements
Your service needs to return an HTTP 200 status code to indicate successful receipt. Any non-200 status code (including 4xx, 5xx) will be considered a failure and trigger the retry mechanism.
Security Verification
Each Webhook request includes the following headers for verifying request origin and preventing replay attacks:
| Header | Description |
|---|---|
X-Client-Request-Id | Unique request ID (UUID format), used for log tracing and idempotent processing |
X-Timestamp | Request timestamp (milliseconds), recommended to verify timeliness (e.g., valid within 5 minutes) to prevent replay attacks |
X-Signature | Request signature (HMAC-SHA256), used to verify request integrity |
Signature Verification
The signature is generated using the HMAC-SHA256 algorithm:
- Signature Key: Webhook signing secret configured in SUNBAY Copilot application details
- Signature Content: Raw request body JSON string (maintain field order and format consistency)
- Signature Format: Hexadecimal lowercase string
Notification Content
The Webhook request body is in JSON format and contains complete transaction information.
All amount fields are integers in the smallest currency unit (e.g., cents for USD, yen for JPY).
Field Description
To ensure backward compatibility, we may add new fields in future versions. Please ensure your parsing logic can ignore unknown fields to avoid parsing failures due to new fields.
| Field | Type | Description |
|---|---|---|
transactionId | string | SUNBAY transaction ID |
transactionRequestId | string | Transaction request ID, used to correlate requests |
referenceOrderId | string | Your business order number |
transactionStatus | string | Transaction status: S success, F failed, C closed |
transactionType | string | Transaction type, such as SALE, AUTH, REFUND, etc. |
createTime | string | Transaction creation time. Format: yyyy-MM-DDTHH:mm:ss+TIMEZONE (ISO 8601) Format: date-time Example: “2023-11-19T10:30:00+08:00” |
completeTime | string | Transaction completion time. Format: yyyy-MM-DDTHH:mm:ss+TIMEZONE (ISO 8601) Format: date-time Example: “2023-11-19T10:32:00+08:00” |
priceCurrency | string | Currency codes (ISO 4217), such as USD, CNY |
orderAmount | integer | Order amount (smallest currency unit) |
transactionAmount | integer | Actual transaction amount (smallest currency unit) |
taxAmount | integer | Tax amount (smallest currency unit) |
surchargeAmount | integer | Surcharge amount (smallest currency unit) |
tipAmount | integer | Tip amount (smallest currency unit) |
cashbackAmount | integer | Cashback amount (smallest currency unit) |
maskedPan | string | Masked card number, e.g., 411111******1111 |
cardNetworkType | string | Card network type, such as VISA, MASTERCARD |
paymentMethod | string | Payment method, such as CARD, WALLET |
subPaymentMethod | string | Sub payment method |
batchNo | integer | Batch number |
voucherNo | string | Voucher number |
stan | string | System trace audit number |
rrn | string | Retrieval reference number |
authCode | string | Authorization code |
entryMode | string | Entry mode, such as SWIPE, INSERT, TAP |
authenticationMethod | string | Authentication method, such as PIN, SIGN |
transactionResultCode | string | Transaction result code |
transactionResultMsg | string | Transaction result description |
terminalSn | string | Terminal serial number |
description | string | Payment description |
attach | string | Additional data (pass-through field) |
Example
A typical Webhook request example:
{
"transactionId": "T202512160001",
"transactionRequestId": "REQ202512160001",
"referenceOrderId": "ORDER_10001",
"transactionStatus": "S",
"transactionType": "SALE",
"createTime": "2023-11-19T10:30:00+08:00",
"completeTime": "2023-11-19T10:30:10+08:00",
"priceCurrency": "USD",
"orderAmount": 1000,
"transactionAmount": 950,
"taxAmount": 50,
"surchargeAmount": 0,
"tipAmount": 0,
"cashbackAmount": 0,
"maskedPan": "411111******1111",
"cardNetworkType": "VISA",
"paymentMethod": "CARD",
"subPaymentMethod": "CREDIT",
"batchNo": 1,
"voucherNo": "000123",
"stan": "000456",
"rrn": "123456789012",
"authCode": "A12345",
"entryMode": "CHIP",
"authenticationMethod": "PIN",
"transactionResultCode": "00",
"transactionResultMsg": "Approved",
"terminalSn": "SN1234567890",
"description": "Payment for order ORDER_10001",
"attach": "custom_data_example"
}Verify Signature
To ensure the request is from SUNBAY, you need to verify the X-Signature header.
Verification Steps
- Get the raw request body (JSON string)
- Use your Webhook signing secret to perform HMAC-SHA256 signature on the request body
- Convert the calculation result to a hexadecimal lowercase string
- Compare with the
X-Signatureheader
Code Examples
Below are complete examples of receiving and verifying Webhook signatures:
Java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@RestController
@RequestMapping("/webhook")
public class WebhookController {
// Signing secret obtained from SUNBAY Copilot application details
private static final String WEBHOOK_SECRET = "your_webhook_secret_key";
@PostMapping("/notify")
public ResponseEntity<Map<String, String>> handleWebhook(HttpServletRequest request) {
try {
// 1. Get request headers
String signature = request.getHeader("X-Signature");
String requestId = request.getHeader("X-Client-Request-Id");
String timestamp = request.getHeader("X-Timestamp");
// 2. Read raw request body (do not deserialize)
String payload = getRequestBody(request);
// 3. Verify signature
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("code", "INVALID_SIGNATURE", "message", "Signature verification failed"));
}
// 4. Verify timeliness (optional, prevent replay attacks)
if (!verifyTimestamp(timestamp)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("code", "EXPIRED", "message", "Request expired"));
}
// 5. Parse request body and process business logic
WebhookNotifyRequest notifyRequest = JSON.parseObject(payload, WebhookNotifyRequest.class);
processWebhook(notifyRequest, requestId);
// 6. Return success response
return ResponseEntity.ok(Map.of("code", "SUCCESS", "message", "Received"));
} catch (Exception e) {
log.error("Failed to process webhook", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("code", "ERROR", "message", "Internal error"));
}
}
/**
* Read raw request body
*/
private String getRequestBody(HttpServletRequest request) throws Exception {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = request.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
return sb.toString();
}
/**
* Verify signature
*/
private boolean verifySignature(String payload, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expectedSignature = bytesToHex(hash);
return expectedSignature.equalsIgnoreCase(signature);
} catch (Exception e) {
log.error("Signature verification error", e);
return false;
}
}
/**
* Convert byte array to hexadecimal string
*/
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* Verify timeliness (optional)
*/
private boolean verifyTimestamp(String timestamp) {
try {
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
// Only accept requests within 5 minutes
return Math.abs(currentTime - requestTime) <= 5 * 60 * 1000;
} catch (Exception e) {
return false;
}
}
/**
* Process business logic (idempotent processing)
*/
private void processWebhook(WebhookNotifyRequest request, String requestId) {
// Check if already processed (idempotent)
if (isAlreadyProcessed(request.getTransactionId())) {
log.info("Transaction already processed: {}", request.getTransactionId());
return;
}
// Execute business logic
updateOrderStatus(request);
// Mark as processed
markAsProcessed(request.getTransactionId());
}
}Important Notes
- Get the raw request body string first, do not deserialize or reorder fields, use it directly for signature verification
- Always verify the signature before processing business logic
- Signature verification failure should return
HTTP 401status code - Business processing exceptions should return
HTTP 500status code
Processing Notifications
Idempotency
Due to network reasons, you may receive multiple Webhook notifications for the same transaction. You need to ensure your processing logic is idempotent to avoid business issues caused by duplicate processing (such as duplicate shipments or duplicate inventory deductions).
Recommended idempotency keys:
transactionIdtransactionRequestIdX-Client-Request-Id
Processing Flow
It is recommended to process Webhook notifications according to the following flow:
- Verify Signature - Confirm the request is from SUNBAY
- Check Idempotency Key - Query locally whether the transaction has been processed
- Execute Business Logic - Update order status, trigger subsequent processes
- Record Processing Status - Mark as processed to prevent duplication
- Return 200 - Inform SUNBAY of successful receipt
Best Practices
- Quick Response: It is recommended to adopt an asynchronous processing mode, return
HTTP 200immediately after receiving the notification, then put the task in a queue for asynchronous processing - Unified Status Management: Transaction results may be obtained through multiple channels (synchronous return, active query, Webhook). It is recommended to use
transactionIdas the primary key for unified status management - Log Recording: Completely record key fields such as
X-Client-Request-IdandtransactionIdfor easy troubleshooting
Retry and Circuit Breaking
Retry Strategy
When your service returns a non-HTTP 200 status code or the request times out, we will automatically retry.
| Retry Count | Retry Interval |
|---|---|
| 1-3 | 5 seconds |
| 4-6 | 30 seconds |
| 7 | 1 minute |
| 8 | 5 minutes |
| 9 | 30 minutes |
| 10 | 2 hours |
| 11 | 4 hours |
| 12 | 6 hours |
Maximum of 12 retries, after which it will be marked as final failure.
Circuit Breaking Mechanism
To protect both systems, we implement a circuit breaking strategy. When a notification address reaches the failure threshold consecutively, notifications to that address will be suspended.
The following are default configurations (may be adjusted based on specific circumstances):
| Failure Count | Circuit Breaking Duration |
|---|---|
| ≥ 20 times | 1 hour |
| ≥ 50 times | 6 hours |
| ≥ 100 times | 24 hours |
Important Notes
- Ensure Webhook service is highly available and quickly returns
HTTP 200status code - Do not rely entirely on Webhook, it is recommended to use transaction query interface for compensation to avoid missing orders
Response Format
We only care about the HTTP 200 status code, the response body content is not mandatory. However, it is recommended to return standard JSON format for easy log analysis and troubleshooting.
Success Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": "SUCCESS",
"message": "Received"
}Failure Response
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"code": "INTERNAL_ERROR",
"message": "Service temporarily unavailable"
}Testing and Go-Live
Testing Checklist
Before going live, please ensure:
- Signature verification logic is correct
- Idempotent processing logic is complete
- Asynchronous processing mechanism is ready
- Log recording is complete (including key fields such as
X-Client-Request-Id,transactionId) - Exception handling (timeout, retry, etc.)
- Monitoring and alerting configuration is complete
Production Environment Recommendations
- High Availability: Ensure Webhook service is stable and available to avoid triggering circuit breaking
- Asynchronous Processing: Interface quickly returns
HTTP 200, business logic is put in queue for asynchronous processing - Monitoring and Alerting: Monitor key metrics such as receipt success rate and processing time
- Fallback Plan: Use transaction query interface to regularly compensate for missed notifications