Webhook Callback
Overview
This document describes the Webhook callback mechanism for the TocoPay payment system. When payment status changes, the system will send asynchronous notifications to the merchant's specified callback URL.
Callback Mechanism
Synchronous Return vs Asynchronous Notification
-
Synchronous Return: Due to the unreliability of frontend redirects, frontend redirects can only serve as an entry point for the merchant's payment result page. The final payment result must be based on asynchronous notifications, not frontend redirects. Business logic should be implemented in asynchronous notification code.
-
Asynchronous Notification: Server-to-server interaction is invisible, unlike page redirect synchronous notifications that can be displayed on pages. After program execution is completed, the page cannot perform page redirects.
Response Parameter Description
| Parameter Name | Parameter | Type | Description |
|---|---|---|---|
| Status Code | status | int | For more error codes, please refer to the status code table |
| Response Data | result | object | Returns JSON string when status code is "success" |
| Data Signature | sign | string | 32-bit uppercase MD5 signature value |
Response Data Description
| Parameter Name | Parameter | Type | Description |
|---|---|---|---|
| Transaction ID | transactionid | int | Order number generated by the payment platform, unique |
| Order ID | orderid | string | Order number generated by the merchant platform, unique |
| Amount | amount | string | Amount submitted by merchant, with two decimal places |
| Real Amount | real_amount | string | Actual amount deposited/deducted from merchant, with two decimal places |
| Custom | custom | string | Returned as-is (empty string must also be transmitted) |
Callback Example
{
"status": 10000,
"result": "{\"transactionid\":\"2063631\",\"orderid\":\"O170556976476860384\",\"amount\":\"60.00\",\"real_amount\":\"52.00\",\"custom\":\"\"}",
"sign": "CE8C0593C09EC3C8643976BAFC145296"
}Signature Verification
Signature Algorithm
- Get the
statusandresultparameters from the callback data (both are string types) - Parameter Sorting: Sort all parameters alphabetically by key name (A -> Z)
- Concatenate the string in the format:
key=value&, then addkey=your_api_secret - Perform MD5 encryption on the concatenated string
- Convert the encryption result to uppercase
- Compare with the
signparameter in the callback data
Note: Parameters must be sorted alphabetically, do not hardcode field order
Sorting Example
Assuming callback data is:
{
"status": 10000,
"result": "{\"transactionid\":\"2063631\",\"orderid\":\"O170556976476860384\",\"amount\":\"60.00\",\"real_amount\":\"52.00\",\"custom\":\"\"}",
"sign": "CE8C0593C09EC3C8643976BAFC145296"
}Correct sorting and concatenation process:
- Extract parameters:
status=10000,result={"transactionid":"2063631",...} - Sort alphabetically:
resultcomes beforestatus(r < s) - Concatenate string:
result={"transactionid":"2063631",...}&status=10000&key=your_api_secret - MD5 encrypt and convert to uppercase
Signature Verification Examples
PHP
<?php
function verifyWebhookSignature($status, $result, $sign, $apiSecret) {
// Build parameter array
$params = [
'status' => $status,
'result' => $result
];
// Sort parameters alphabetically by key name (A -> Z)
ksort($params);
// Build signature string
$dataString = '';
foreach ($params as $key => $value) {
$dataString .= $key . '=' . $value . '&';
}
$dataString .= 'key=' . $apiSecret;
// Generate signature
$calculatedSign = strtoupper(md5($dataString));
// Verify signature
return $calculatedSign === $sign;
}
// Usage example
$webhookData = json_decode(file_get_contents('php://input'), true);
$status = $webhookData['status'];
$result = $webhookData['result'];
$sign = $webhookData['sign'];
$apiSecret = 'your_api_secret';
if (verifyWebhookSignature($status, $result, $sign, $apiSecret)) {
// Signature verification successful, process business logic
$resultData = json_decode($result, true);
// Determine transaction result based on status code
if ($status == 10000) {
// Transaction successful
$transactionId = $resultData['transactionid'];
$orderId = $resultData['orderid'];
$amount = $resultData['amount'];
$realAmount = $resultData['real_amount'];
$custom = $resultData['custom'];
// Process business logic
// ...
}
// Return success response
echo 'success';
} else {
// Signature verification failed
http_response_code(400);
echo 'signature verification failed';
}
?>Node.js
const crypto = require('crypto');
function verifyWebhookSignature(status, result, sign, apiSecret) {
// Build parameter object
const params = {
status: status,
result: result
};
// Sort parameters alphabetically by key name (A -> Z)
const sortedKeys = Object.keys(params).sort();
// Build signature string
let dataString = '';
sortedKeys.forEach(key => {
dataString += `${key}=${params[key]}&`;
});
dataString += `key=${apiSecret}`;
// Generate signature
const calculatedSign = crypto.createHash('md5').update(dataString).digest('hex').toUpperCase();
// Verify signature
return calculatedSign === sign;
}
// Usage example (Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const { status, result, sign } = req.body;
const apiSecret = 'your_api_secret';
if (verifyWebhookSignature(status, result, sign, apiSecret)) {
// Signature verification successful, process business logic
const resultData = JSON.parse(result);
// Determine transaction result based on status code
if (status === '10000') {
// Transaction successful
const { transactionid, orderid, amount, real_amount, custom } = resultData;
// Process business logic
// ...
}
// Return success response
res.status(200).send('success');
} else {
// Signature verification failed
res.status(400).send('signature verification failed');
}
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.TreeMap;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WebhookHandler {
public static boolean verifyWebhookSignature(String status, String result, String sign, String apiSecret) {
try {
// Build parameter map
TreeMap<String, String> params = new TreeMap<>();
params.put("status", status);
params.put("result", result);
// Build signature string (TreeMap automatically sorts by key)
StringBuilder dataString = new StringBuilder();
for (String key : params.keySet()) {
dataString.append(key).append("=").append(params.get(key)).append("&");
}
dataString.append("key=").append(apiSecret);
// Generate signature
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(dataString.toString().getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
String calculatedSign = hexString.toString().toUpperCase();
// Verify signature
return calculatedSign.equals(sign);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
// Example data
String status = "10000";
String result = "{\"transactionid\":\"2063631\",\"orderid\":\"O170556976476860384\",\"amount\":\"60.00\",\"real_amount\":\"52.00\",\"custom\":\"\"}";
String sign = "CE8C0593C09EC3C8643976BAFC145296";
String apiSecret = "your_api_secret";
if (verifyWebhookSignature(status, result, sign, apiSecret)) {
System.out.println("Signature verification successful");
// Parse result data
ObjectMapper mapper = new ObjectMapper();
try {
ResultData resultData = mapper.readValue(result, ResultData.class);
System.out.println("Transaction ID: " + resultData.getTransactionid());
System.out.println("Order ID: " + resultData.getOrderid());
System.out.println("Amount: " + resultData.getAmount());
System.out.println("Real Amount: " + resultData.getReal_amount());
System.out.println("Custom Data: " + resultData.getCustom());
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("Signature verification failed");
}
}
// Result data class
public static class ResultData {
private String transactionid;
private String orderid;
private String amount;
private String real_amount;
private String custom;
// Getters and Setters
public String getTransactionid() { return transactionid; }
public void setTransactionid(String transactionid) { this.transactionid = transactionid; }
public String getOrderid() { return orderid; }
public void setOrderid(String orderid) { this.orderid = orderid; }
public String getAmount() { return amount; }
public void setAmount(String amount) { this.amount = amount; }
public String getReal_amount() { return real_amount; }
public void setReal_amount(String real_amount) { this.real_amount = real_amount; }
public String getCustom() { return custom; }
public void setCustom(String custom) { this.custom = custom; }
}
}Asynchronous Notification Requirements
-
Page Requirements: The asynchronous notification page and URL (notify_url) must not contain any characters such as spaces, HTML tags, parameters, or exception messages thrown by the development system.
-
Interaction Method: Server-to-server interaction is invisible, unlike page redirect synchronous notifications that can be displayed on pages. After program execution is completed, the page cannot perform page redirects.
-
Trigger Condition: Notifications are only sent after transaction payment is completed.
-
Environment Requirements: Please do not filter or intercept this address. Cookies, sessions, etc. will be invalid on this page, meaning these data cannot be accessed.
-
Debugging Requirements: Debugging and running must be done on a server that is accessible from the internet.
-
Signature Verification: After receiving asynchronous notifications, the merchant system must verify the signature (verify the sign parameter in the notification) to ensure that the payment notification was sent by us.
-
Response Requirements: After program execution is completed, you must return "success" (without quotes, case-sensitive), otherwise we will resend the asynchronous notification three times.
Important Reminders
-
Parameter Sorting: Must sort all parameters alphabetically by key name (A -> Z), do not hardcode field order
-
Signature Composition: When composing the sign, only process the status and result parameters (both are strings). Do not include the sign itself in the encryption.
-
JSON Processing: Do not parse the result JSON data into objects before encryption.
-
Status Judgment: The status code represents the transaction status of this order. Please use this as the basis for determining whether the transaction is successful.
Common Status Codes
| Status Code | Description |
|---|---|
| 10000 | Transaction successful |
| 20001 | Transaction failed |
| 20002 | Transaction processing |
| 20003 | Transaction timeout |
| 20004 | Transaction cancelled |
Error Handling
Common Issues
-
Signature Verification Failed:
- Check if the API key is correct
- Confirm parameters are sorted alphabetically (A -> Z)
- Check if the signature algorithm is implemented according to the documentation requirements
- Do not hardcode field order
-
Duplicate Notifications: Ensure the callback interface returns the "success" string.
-
Network Timeout: Ensure the callback URL is accessible and set a reasonable timeout.
-
Data Parsing Error: Ensure the result field is processed as a JSON string, do not pre-parse it.
-
Sorting Error: The most common cause of signature verification failure is incorrect parameter sorting. Please use dynamic sorting instead of fixed order.
Debugging Tips
- Log all received callback data
- Log parameter sorting process: Record parameter order before and after sorting
- Log signature string: Record the complete signature string for comparison
- Log the signature verification process
- Log business processing results
- Monitor callback interface response times
Support
If you encounter any issues while implementing Webhook callbacks, please contact our technical support team.
Updated 4 months ago