Signature
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
| Step | Description |
|---|---|
| 1. Generate a key pair | Use RSA PKCS#8 (recommended 2048-bit). Keep the merchant private key secure. |
| 2. Configure the public key | Upload the merchant public key in the TOPPAY Merchant Dashboard (only the public key is stored on the platform). |
| 3. Difference of two signatures | Request 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 key | Find it under Dashboard → API settings for verifying asynchronous notifications. |
Technical summary
| Item | Description |
|---|---|
| Algorithm | RSA (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) |
| Encoding | Signature output is Base64 |
| Private key format | PKCS#8 (recommended 2048-bit; legacy merchants may use 1024-bit) |
Core rules (must follow)
- Only parameter values are concatenated: parameter names (keys) are used for sorting only and must not appear in the string to sign.
- Sort keys of all non-empty parameters in ascending ASCII order.
- 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.
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
| mchNo | String(32) | Y | Merchant ID, from merchant portal – profile | PHOT000001 |
| method | String(16) | N | Payment method | GCASH |
| orderNum | String(64) | Y | Merchant order number | T1642592278863 |
| amount | int(10) | Y | Amount (per API contract) | 10000 |
| productDetail | String(100) | Y | Product / order description | Test Pay |
| downNotifyUrl | String(164) | Y | Async notification URL | your notify url |
| timestamp | String(32) | Y | Request timestamp (13-digit millisecond Unix timestamp) | 1749451858772 |
| customerName | String(64) | Y | Customer name | JackMa |
| expiryPeriod | int(5) | Y | Order validity (minutes) | 1440 |
| customerEmail | String(64) | Y | Customer 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
10000and1440contribute as the character sequences for"10000"and"1440". - Exclude
signbefore 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:
| Order | Key | Value |
|---|---|---|
| 1 | amount | 10000 |
| 2 | customerEmail | [email protected] |
| 3 | customerName | JackMa |
| 4 | downNotifyUrl | your notify url |
| 5 | expiryPeriod | 1440 |
| 6 | mchNo | PHOT000001 |
| 7 | method | GCASH |
| 8 | orderNum | T1642592278863 |
| 9 | productDetail | Test Pay |
| 10 | timestamp | 1749451858772 |
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
| Type | Who holds private key | Who holds public key | Your action |
|---|---|---|---|
| Request signing | Merchant | Platform (merchant public key) | Sign with merchant private key and send sign |
| Callback verification | Platform | Merchant (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
signin the Signature: Thesignparameter 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
| Symptom | What to do |
|---|---|
| Signature mismatch | Log StrA and compare step-by-step with the doc; confirm key order, values only, no separators |
| Invalid timestamp | Sync NTP; timestamp must be a 13-digit millisecond Unix timestamp |
| Key format errors | Private key must be PKCS#8; paste full Base64 and PEM wrapping as in examples |
| Empty values | null, empty string, and omitted fields are excluded; do not sign placeholder empty strings |
| Numbers in Java | If using JsonObject.getAsString(), ensure numeric JSON fields are handled consistently, or normalize to string before signing |
For a bilingual reference, see the Chinese version.
