Skip to main content

Signature

TOPPAY TeamAbout 6 min

Overview

Digital signatures protect API requests (pay-in, pay-out, inquiry, etc.) for integrity and non-repudiation. You must generate sign exactly as described here; otherwise the API returns signature verification failure.

Before you integrate

StepDescription
1. Generate a key pairUse RSA PKCS#8 (recommended 2048-bit). Keep the merchant private key secure.
2. Configure the public keyUpload the merchant public key in the TOPPAY Merchant Dashboard (only the public key is stored on the platform).
3. Difference of two signaturesRequest signing: you sign with your private key. Callback verification: verify callbacks using the platform public key (field may be sign or as documented).
4. Platform public keyFind it under Dashboard → API settings for verifying asynchronous notifications.

Technical summary

ItemDescription
AlgorithmRSA (sender encrypts with private key to produce the signature; receiver decrypts with public key to compare — implement exactly as in the sample code; do not confuse with standard RSA-SHA256 Sign/Verify)
EncodingSignature output is Base64
Private key formatPKCS#8 (recommended 2048-bit; legacy merchants may use 1024-bit)

Core rules (must follow)

  1. Only parameter values are concatenated: parameter names (keys) are used for sorting only and must not appear in the string to sign.
  2. Sort keys of all non-empty parameters in ascending ASCII order.
  3. In that order, concatenate values only into the string to sign, then RSA-encrypt with your merchant private key to obtain sign.

Example: from parameters to signature

Sample field reference

The following parameters correspond to the fields used in pay-in APIs. They are provided for reference and comparison with the official documentation.Whether a parameter is required or optional depends on the specific API being called.

ParameterTypeRequiredDescriptionExample
mchNoString(32)YMerchant ID, from merchant portal – profilePHOT000001
methodString(16)NPayment methodGCASH
orderNumString(64)YMerchant order numberT1642592278863
amountint(10)YAmount (per API contract)10000
productDetailString(100)YProduct / order descriptionTest Pay
downNotifyUrlString(164)YAsync notification URLyour notify url
timestampString(32)YRequest timestamp (13-digit millisecond Unix timestamp)1749451858772
customerNameString(64)YCustomer nameJackMa
expiryPeriodint(5)YOrder validity (minutes)1440
customerEmailString(64)YCustomer email[email protected]

Sample JSON (do not include sign while computing the signature):

{
  "mchNo": "PHOT000001",
  "method": "GCASH",
  "orderNum": "T1642592278863",
  "amount": 10000,
  "productDetail": "Test Pay",
  "downNotifyUrl": "your notify url",
  "timestamp": "1749451858772",
  "customerName": "JackMa",
  "expiryPeriod": 1440,
  "customerEmail": "[email protected]"
}

Values and types

  • Concatenate the string form of values as sent (consistent with JSON serialization). For example, numbers 10000 and 1440 contribute as the character sequences for "10000" and "1440".
  • Exclude sign before signing; empty strings, null, and omitted fields do not participate.

Step 1: Sort keys in ascending ASCII order

Sort keys of all non-empty parameters:

OrderKeyValue
1amount10000
2customerEmail[email protected]
3customerNameJackMa
4downNotifyUrlyour notify url
5expiryPeriod1440
6mchNoPHOT000001
7methodGCASH
8orderNumT1642592278863
9productDetailTest Pay
10timestamp1749451858772

Step 2: Build string StrA

Concatenate values only, in order, with no separators:

StrA = [email protected] notify url1440PHOT000001GCASHT1642592278863Test Pay1749451858772

Notes

  • Concatenate values only, not keys.
  • Empty values are omitted from sorting and concatenation.
  • There are no separators between values (no &, =, newlines, etc.).

Step 3: Compute the signature

Encrypt StrA with your merchant private key configured in TOPPAY Merchant Dashboard, then Base64-encode:

sign = RSA(StrA, privateKey)

Put sign in the request body (field name is usually sign unless the API says otherwise).

Signature output example

IMLn23c4orM+7pZhHoRmbjrol4X33jeAqFxbZuQ+pnznBIGhb6Ail3qQPmKwcuhNCt536nmldpbWI72
k1lDxd0zZ95ZHElcNzwTFHFKtd8063uy6rFaxaW6DQ47t4U/95dpGfHAZe0GiIFAQ6xQquaoLINyQa4QqL+cpB
JFEg1dyW6GYLFSdJnx7ycQvFYllmOpGZmdPLny62GvrCWvkiIARUsmc9fpkpTx5UQEDTgmhwdCKBkhHVsx2AiQ
bYDxZ5WBuU1GZeiJjPuzSxvzWP6VoQBsfpwTI5kdJs6aQCekGO2/YScD+tGgrm2J89Pc/axPcb1xZzsi5SxpWh
feabQ==

Sample request JSON including sign

{
  "mchNo": "PHOT000001",
  "method": "GCASH",
  "orderNum": "T1642592278863",
  "amount": 10000,
  "productDetail": "Test Pay",
  "downNotifyUrl": "your notify url",
  "timestamp": "1749451858772",
  "customerName": "JackMa",
  "expiryPeriod": 1440,
  "customerEmail": "[email protected]",
  "sign": "IMLn23c4orM+7pZhHoRmbjrol4X33jeAqFxbZuQ+pnznBIGhb6Ail3qQPmKwcuhNCt536nmldpbWI72k1lDxd0zZ95ZHElcNzwTFHFKtd8063uy6rFaxaW6DQ47t4U/95dpGfHAZe0GiIFAQ6xQquaoLINyQa4QqL+cpBJFEg1dyW6GYLFSdJnx7ycQvFYllmOpGZmdPLny62GvrCWvkiIARUsmc9fpkpTx5UQEDTgmhwdCKBkhHVsx2AiQbYDxZ5WBuU1GZeiJjPuzSxvzWP6VoQBsfpwTI5kdJs6aQCekGO2/YScD+tGgrm2J89Pc/axPcb1xZzsi5SxpWhfeabQ=="
}

Request signing vs callback verification

TypeWho holds private keyWho holds public keyYour action
Request signingMerchantPlatform (merchant public key)Sign with merchant private key and send sign
Callback verificationPlatformMerchant (platform public key)Remove sign from the payload, build the same concatenation from remaining fields, decrypt sign with platform public key and compare

For callbacks: extract sign first, sort remaining non-empty keys in ASCII order, concatenate values, then verify with verifySign logic below.


Code examples

Sample note

DefaultHttpClient (Apache HttpClient 4.x) in the sample below is deprecated and shown for signing logic only. Use HttpClient 5.x or your project's HTTP client in production.

import com.google.gson.JsonObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * TOPPAY RSA signature utility
 *
 * @author TOPPAY
 */
public class TOPPAYRequestUtil {

    /**
     * Generate request signature
     */
    public static String generateSign(JsonObject params, String privateKey)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        List<String> paramNameList = new ArrayList<>(params.keySet());
        Collections.sort(paramNameList);

        StringBuilder stringBuilder = new StringBuilder();
        for (String name : paramNameList) {
            if (params.get(name) != null && !params.get(name).isJsonNull()) {
                String value = params.get(name).getAsString();
                if (value != null && !value.isEmpty()) {
                    stringBuilder.append(value);
                }
            }
        }
        String strA = stringBuilder.toString();
        System.out.println("String to sign: " + strA);

        return privateEncrypt(strA, getPrivateKey(privateKey));
    }

    /**
     * Verify callback signature (removes sign, rebuilds string, compares with decrypted sign)
     */
    public static boolean verifySign(JsonObject params, String publicKey)
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        String sign = params.remove("sign").getAsString();
        List<String> paramNameList = new ArrayList<>(params.keySet());
        Collections.sort(paramNameList);

        StringBuilder stringBuilder = new StringBuilder();
        for (String name : paramNameList) {
            if (params.get(name) != null && !params.get(name).isJsonNull()) {
                String value = params.get(name).getAsString();
                if (value != null && !value.isEmpty()) {
                    stringBuilder.append(value);
                }
            }
        }
        System.out.println("Verification string: " + stringBuilder);

        String decryptSign = publicDecrypt(sign, getPublicKey(publicKey));
        System.out.println("Decrypted signature: " + decryptSign);

        return stringBuilder.toString().equals(decryptSign);
    }

    public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE,
                    data.getBytes("UTF-8"), privateKey.getModulus().bitLength()));
        } catch (Exception e) {
            throw new RuntimeException("Error encrypting string [" + data + "]", e);
        }
    }

    public static String publicDecrypt(String data, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE,
                    Base64.decodeBase64(data), publicKey.getModulus().bitLength()), "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException("Error decrypting string [" + data + "]", e);
        }
    }

    public static RSAPrivateKey getPrivateKey(String privateKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
    }

    public static RSAPublicKey getPublicKey(String publicKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
        return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
    }

    private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
        int maxBlock = opmode == Cipher.DECRYPT_MODE ? keySize / 8 : keySize / 8 - 11;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] buff;
        int i = 0;
        try {
            while (datas.length > offSet) {
                if (datas.length - offSet > maxBlock) {
                    buff = cipher.doFinal(datas, offSet, maxBlock);
                } else {
                    buff = cipher.doFinal(datas, offSet, datas.length - offSet);
                }
                out.write(buff, 0, buff.length);
                i++;
                offSet = i * maxBlock;
            }
        } catch (Exception e) {
            throw new RuntimeException("Error processing block size [" + maxBlock + "]", e);
        }
        byte[] resultDatas = out.toByteArray();
        IOUtils.closeQuietly(out);
        return resultDatas;
    }

    public static String doPost(String url, String json) throws IOException {
        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(url);
        StringEntity s = new StringEntity(json);
        s.setContentEncoding("UTF-8");
        s.setContentType("application/json");
        post.setEntity(s);
        HttpResponse res = client.execute(post);
        if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
            return EntityUtils.toString(res.getEntity());
        }
        return null;
    }
}

@tab PHP

<?php
/**
 * TOPPAY RSA signature utility
 *
 * @author TOPPAY
 */
class TOPPAYRequestUtil
{
    public static function generateSign(array $params, string $privateKey): string
    {
        $keys = array_keys($params);
        sort($keys, SORT_STRING);

        $strA = '';
        foreach ($keys as $key) {
            $value = $params[$key];
            if ($value !== null && $value !== '') {
                $strA .= $value;
            }
        }

        $privateKeyResource = openssl_pkey_get_private(
            "-----BEGIN PRIVATE KEY-----\n" .
            wordwrap($privateKey, 64, "\n", true) .
            "\n-----END PRIVATE KEY-----"
        );

        $encrypted = '';
        $data = $strA;
        $keyDetails = openssl_pkey_get_details($privateKeyResource);
        $maxBlockSize = $keyDetails['bits'] / 8 - 11;

        $output = '';
        while (strlen($data) > 0) {
            $chunk = substr($data, 0, $maxBlockSize);
            $data = substr($data, $maxBlockSize);
            openssl_private_encrypt($chunk, $encrypted, $privateKeyResource);
            $output .= $encrypted;
        }

        return base64_encode($output);
    }

    public static function verifySign(array $params, string $publicKey): bool
    {
        $sign = $params['sign'];
        unset($params['sign']);

        $keys = array_keys($params);
        sort($keys, SORT_STRING);

        $strA = '';
        foreach ($keys as $key) {
            $value = $params[$key];
            if ($value !== null && $value !== '') {
                $strA .= $value;
            }
        }

        $publicKeyResource = openssl_pkey_get_public(
            "-----BEGIN PUBLIC KEY-----\n" .
            wordwrap($publicKey, 64, "\n", true) .
            "\n-----END PUBLIC KEY-----"
        );

        $encryptedData = base64_decode($sign);
        $keyDetails = openssl_pkey_get_details($publicKeyResource);
        $maxBlockSize = $keyDetails['bits'] / 8;

        $decrypted = '';
        while (strlen($encryptedData) > 0) {
            $chunk = substr($encryptedData, 0, $maxBlockSize);
            $encryptedData = substr($encryptedData, $maxBlockSize);
            $decryptedChunk = '';
            openssl_public_decrypt($chunk, $decryptedChunk, $publicKeyResource);
            $decrypted .= $decryptedChunk;
        }

        return $strA === $decrypted;
    }

    public static function doPost(string $url, array $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Accept: application/json'
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode === 200) {
            return $response;
        }
        return false;
    }
}

$params = [
    'mchNo' => 'PHOT000001',
    'method' => 'GCASH',
    'orderNum' => 'T1642592278863',
    'amount' => 10000,
    'productDetail' => 'Test Pay',
    'downNotifyUrl' => 'your notify url',
    'timestamp' => '1749451858772',
    'customerName' => 'JackMa',
    'expiryPeriod' => 1440,
    'customerEmail' => '[email protected]'
];

$privateKey = 'YOUR_PRIVATE_KEY_HERE';
$sign = TOPPAYRequestUtil::generateSign($params, $privateKey);
$params['sign'] = $sign;

$response = TOPPAYRequestUtil::doPost('https://gateway.TOPPAY.com/v2.0/transaction/pay-in', $params);

:::


Security and troubleshooting

Common Integration Errors

Note

  • Including sign in the Signature: The sign parameter must be removed before computing the signature.
  • Incorrect Sorting Order: Keys must be sorted in ASCII key order,ascending order, not by business logic or the order in documentation tables.
  • Extra Spaces or Line Breaks: Leading or trailing spaces in parameter values are included in the concatenation, which may cause a mismatch with the backend.
  • Incorrect Encoding: The string to be signed must be UTF-8 encoded.
  • Re-sign After Modification: Any change to the request body requires recomputing the sign.

FAQ

SymptomWhat to do
Signature mismatchLog StrA and compare step-by-step with the doc; confirm key order, values only, no separators
Invalid timestampSync NTP; timestamp must be a 13-digit millisecond Unix timestamp
Key format errorsPrivate key must be PKCS#8; paste full Base64 and PEM wrapping as in examples
Empty valuesnull, empty string, and omitted fields are excluded; do not sign placeholder empty strings
Numbers in JavaIf using JsonObject.getAsString(), ensure numeric JSON fields are handled consistently, or normalize to string before signing

For a bilingual reference, see the Chinese version.