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 NameParameterTypeDescription
Status CodestatusintFor more error codes, please refer to the status code table
Response DataresultobjectReturns JSON string when status code is "success"
Data Signaturesignstring32-bit uppercase MD5 signature value

Response Data Description

Parameter NameParameterTypeDescription
Transaction IDtransactionidintOrder number generated by the payment platform, unique
Order IDorderidstringOrder number generated by the merchant platform, unique
AmountamountstringAmount submitted by merchant, with two decimal places
Real Amountreal_amountstringActual amount deposited/deducted from merchant, with two decimal places
CustomcustomstringReturned 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

  1. Get the status and result parameters from the callback data (both are string types)
  2. Parameter Sorting: Sort all parameters alphabetically by key name (A -> Z)
  3. Concatenate the string in the format: key=value&, then add key=your_api_secret
  4. Perform MD5 encryption on the concatenated string
  5. Convert the encryption result to uppercase
  6. Compare with the sign parameter 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:

  1. Extract parameters: status=10000, result={"transactionid":"2063631",...}
  2. Sort alphabetically: result comes before status (r < s)
  3. Concatenate string: result={"transactionid":"2063631",...}&status=10000&key=your_api_secret
  4. 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

  1. 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.

  2. 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.

  3. Trigger Condition: Notifications are only sent after transaction payment is completed.

  4. 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.

  5. Debugging Requirements: Debugging and running must be done on a server that is accessible from the internet.

  6. 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.

  7. 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

  1. Parameter Sorting: Must sort all parameters alphabetically by key name (A -> Z), do not hardcode field order

  2. 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.

  3. JSON Processing: Do not parse the result JSON data into objects before encryption.

  4. 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 CodeDescription
10000Transaction successful
20001Transaction failed
20002Transaction processing
20003Transaction timeout
20004Transaction cancelled

Error Handling

Common Issues

  1. 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
  2. Duplicate Notifications: Ensure the callback interface returns the "success" string.

  3. Network Timeout: Ensure the callback URL is accessible and set a reasonable timeout.

  4. Data Parsing Error: Ensure the result field is processed as a JSON string, do not pre-parse it.

  5. Sorting Error: The most common cause of signature verification failure is incorrect parameter sorting. Please use dynamic sorting instead of fixed order.

Debugging Tips

  1. Log all received callback data
  2. Log parameter sorting process: Record parameter order before and after sorting
  3. Log signature string: Record the complete signature string for comparison
  4. Log the signature verification process
  5. Log business processing results
  6. Monitor callback interface response times

Support

If you encounter any issues while implementing Webhook callbacks, please contact our technical support team.