Callback Signature
Sekitar 2 menit
Callback Signature
Applies to API v2.0 (RSA). When we POST to your notify URL, verify X-SIGNATURE before updating the order.
Not the same as request signing
Callback is not timestamp|merchant_secret|body. Formula is only tradeNo|X-TIMESTAMP, verified with Platform Public Key. Request signing: Request Signature.
Formula
Platform signs:
stringToSign = tradeNo + "|" + X-TIMESTAMP
| Part | Source |
|---|---|
tradeNo | JSON body field tradeNo |
X-TIMESTAMP | HTTP header X-TIMESTAMP (same pattern as request: yyyy-MM-dd'T'HH:mm:ssXXX) |
Example shape:
{tradeNo}|{X-TIMESTAMP}
Not the same as request signing
- Only these 2 parts — no body minify, no
merchant_secret - Verify with Platform Public Key (not your merchant private key)
Platform Public Key
Copy Platform Public Key from Merchant Portal → Configuration Info.
Sandbox and Production keys are different. Details: Integration Info.
Code samples
Open your language. platformPublicKeyBase64 is the Platform Public Key copied from Configuration Info (raw Base64).
Java
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/** Verify callback X-SIGNATURE. */
public class CallbackVerifier {
public static boolean verify(
String tradeNo,
String timestamp,
String signatureBase64,
String platformPublicKeyBase64) throws Exception {
// tradeNo|X-TIMESTAMP
String stringToSign = tradeNo + "|" + timestamp;
byte[] keyBytes = Base64.getDecoder().decode(platformPublicKeyBase64);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(stringToSign.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(signatureBase64));
}
}
PHP
<?php
/** Verify callback X-SIGNATURE. $platformPublicKeyBase64 = raw Base64 from portal. */
function verifyCallback(
string $tradeNo,
string $timestamp,
string $signatureBase64,
string $platformPublicKeyBase64
): bool {
// tradeNo|X-TIMESTAMP
$stringToSign = $tradeNo . '|' . $timestamp;
$keyPem = "-----BEGIN PUBLIC KEY-----
"
. chunk_split($platformPublicKeyBase64, 64, "
")
. "-----END PUBLIC KEY-----
";
$publicKey = openssl_pkey_get_public($keyPem);
if ($publicKey === false) {
return false;
}
return openssl_verify($stringToSign, base64_decode($signatureBase64), $publicKey, OPENSSL_ALGO_SHA256) === 1;
}
Python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
def verify_callback(
trade_no: str,
timestamp: str,
signature_base64: str,
public_key_base64: str,
) -> bool:
"""Verify callback X-SIGNATURE. public_key_base64 = Platform Public Key from portal."""
# tradeNo|X-TIMESTAMP
string_to_sign = f"{trade_no}|{timestamp}"
public_key = serialization.load_der_public_key(base64.b64decode(public_key_base64))
try:
public_key.verify(
base64.b64decode(signature_base64),
string_to_sign.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except InvalidSignature:
return False
Node.js
const crypto = require("crypto");
/** Verify callback X-SIGNATURE. platformPublicKeyBase64 = Platform Public Key from portal. */
function verifyCallback(tradeNo, timestamp, signatureBase64, platformPublicKeyBase64) {
// tradeNo|X-TIMESTAMP
const stringToSign = `${tradeNo}|${timestamp}`;
const publicKey = crypto.createPublicKey({
key: Buffer.from(platformPublicKeyBase64, "base64"),
format: "der",
type: "spki",
});
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(stringToSign, "utf8");
return verifier.verify(publicKey, signatureBase64, "base64");
}
Golang
package signature
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
)
// VerifyCallback verifies callback X-SIGNATURE. platformPublicKeyBase64 = Platform Public Key from portal.
func VerifyCallback(tradeNo, timestamp, signatureBase64, platformPublicKeyBase64 string) (bool, error) {
// tradeNo|X-TIMESTAMP
stringToSign := tradeNo + "|" + timestamp
keyBytes, err := base64.StdEncoding.DecodeString(platformPublicKeyBase64)
if err != nil {
return false, err
}
key, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return false, err
}
pubKey, ok := key.(*rsa.PublicKey)
if !ok {
return false, fmt.Errorf("not an RSA public key")
}
sigBytes, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return false, err
}
hashed := sha256.Sum256([]byte(stringToSign))
if err := rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sigBytes); err != nil {
return false, nil
}
return true, nil
}
After verify — return SUCCESS
Response body must be exactly SUCCESS after trim (case-sensitive). Not JSON. Full flow: Callback Notification.
Quick troubleshooting
| Problem | Check |
|---|---|
| Callback verify always fails | Used request formula — use tradeNo|X-TIMESTAMP + Platform Public Key for that environment |
| Callbacks keep retrying | Body must be exact text SUCCESS (trimmed), not JSON |

