Skip to main content

Callback Signature

ประมาณ 2 นาที

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
PartSource
tradeNoJSON body field tradeNo
X-TIMESTAMPHTTP 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

ProblemCheck
Callback verify always failsUsed request formula — use tradeNo|X-TIMESTAMP + Platform Public Key for that environment
Callbacks keep retryingBody must be exact text SUCCESS (trimmed), not JSON