API Signature Specification
Scope & Roles
- Applicable APIs: Veloraya collection (Payin), payout, query, balance, bank list and all callback notifications for Mexico.
- Goal: Help merchants integrate signing quickly, troubleshoot issues, and meet security audit requirements.
- Key roles:
- Merchant: initiates API requests, generates signatures, and securely keeps the private key.
- Veloraya: verifies merchant requests, pushes callbacks to merchants, and signs them with the platform private key.
Flow Overview
Merchant builds the request
- Generate the request body JSON (
body) from business parameters. - Generate
timestampandnonce, then build the signature plaintext per the spec.
Merchant signs and sends the request
- Sign the plaintext with the merchant private key + SHA256withRSA, Base64-encode it to get
Signature. - Send the request with headers
MerchantId / Timestamp / Nonce / Signatureand the body to Veloraya.
Veloraya verifies and processes
- Validate timestamp, nonce and signature.
- If valid, execute routing, risk control, accounting and return the synchronous response.
Veloraya sends callbacks
- When order status changes, Veloraya builds the callback body, generates timestamp/nonce, signs with the platform private key and sends it to the merchant.
- The merchant verifies the callback with the Veloraya public key and updates local order status.
Prerequisites
Request method & network
- Read/write APIs use POST by default; balance query and payout-bank query use GET.
- All traffic must use HTTPS; agree the egress IP whitelist with Veloraya.
Required headers
| Header | Required | Type | Description |
|---|---|---|---|
| Content-Type | Y | string | Fixed to application/json |
| MerchantId | Y | string | Merchant ID assigned by Veloraya |
| Timestamp | Y | string | Millisecond timestamp; must be close to server time |
| Nonce | Y | string | Unique random string |
| Signature | Y | string | Request signature, Base64-encoded RSA result |
Data spec
- The body used for signing must be identical to the actual request body (field order, whitespace, escaping).
- For GET requests without a body,
bodyin the signing plaintext is"".
Request Signing Guide (Merchant side)
Signing plaintext format
merchantId={merchantId},timestamp={timestamp},nonce={nonce},body={requestBodyJson}Steps
- Build plaintext: concatenate per the format above.
- UTF-8 encode: convert the plaintext to UTF-8 bytes.
- Sign: use SHA256withRSA with the merchant private key.
- Base64 encode: encode the signature bytes to Base64 as the
Signatureheader.
POST request signing
POST requests (payin, payout, query etc.) include a JSON body; sign with the full JSON string as the body parameter.
Important
- Field order in the request body JSON is not required to be sorted.
- The JSON string used for signing must be exactly the same order as the actual request body.
- The server verifies the signature against the raw request body (it never reorders fields), ensuring the same order as the client.
POST request signing example
Request example:
POST /api/payin HTTP/1.1
Content-Type: application/json
MerchantId: 101111
Timestamp: 1703123456789
Nonce: ITqRczTuacKWhOuppvpVIsBXfHhkVXcs
Signature: xxxxxx...
{
"amount": 500,
"currency": "THB",
"merchantOrderId": "ORDER5187977827671717",
"payinMethod": "PromptPay QR",
"payerEmail": "user@example.com",
"payerIdType": "passport",
"payerIdCard": "650100201901155112",
"payerMobile": "05468548186",
"payerName": "Apiratee Rodkaew",
"payToken": "",
"expireSeconds": 3600,
"redirectUrl": "https://www.xxx.com"
}Signing plaintext:
merchantId=101111,timestamp=1703123456789,nonce=ITqRczTuacKWhOuppvpVIsBXfHhkVXcs,
body={"amount": 500,"currency": "THB","merchantOrderId": "ORDER5187977827671717","payinMethod": "PromptPay QR","payerEmail": "user@example.com","payerIdType": "passport","payerIdCard": "650100201901155112","payerMobile": "05468548186","payerName": "Apiratee Rodkaew","payToken": "","expireSeconds": 3600,"redirectUrl": "https://www.xxx.com"}GET request signing
GET requests (balance, bank query) have no request body, but the signing plaintext must still include body= (empty string) to keep the format consistent.
Design principles:
- Unified format: POST and GET use the same signing plaintext format for simpler verification logic.
- Explicitness: explicitly including
body=avoids ambiguity and prevents signature verification failures.
GET request signing example
Request example:
GET /api/balance?currency=THB HTTP/1.1
Content-Type: application/json
MerchantId: 101111
Timestamp: 1703123456789
Nonce: sVEGRQHPAonwRnDHQhtAJmKoqRhNMRUn
Signature: xxxxxx...Signing plaintext:
merchantId=101111,timestamp=1703123456789,nonce=abc123def456,body=Unified Code Example
The following code supports both POST and GET request signing:
java
/**
* Generate an API request signature (supports POST and GET)
* @param merchantId merchant ID
* @param bodyJson request body JSON string; pass "" for GET requests
* @param merchantPrivateKeyBase64 Base64-encoded merchant private key
* @return Base64-encoded signature
*/
public String signRequest(String merchantId, String bodyJson,
String merchantPrivateKeyBase64) throws Exception {
// Step 1: generate timestamp and nonce
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
// Step 2: generate the signature via the helper
// Note: bodyJson should be "" for GET requests
String signBase64 = VelorayaSignUtils.generateApiSign(
merchantId, timestamp, nonce, bodyJson, merchantPrivateKeyBase64
);
// Step 3: put MerchantId/timestamp/nonce/Signature into request headers and send
return signBase64;
}Callback Verification Guide
- Read
Signature,Timestamp,Noncefrom the callback headers (callbacks do not contain merchantId). - Build the plaintext
timestamp={...},nonce={...},body={requestBodyJson}. - We recommend reusing
VelorayaSignUtils.verifyCallbackSign(...)from Appendix B, which implements the above steps and SHA256withRSA verification.
Security & Risk Control
- Time sync: keep your server time in sync with Veloraya; requests with excessive drift are rejected.
- Replay protection: Nonce must be unique; we recommend short-term dedup caching at the business layer.
- Transport security: enforce HTTPS; whitelist necessary egress IPs.
Troubleshooting
| Scenario | Symptom | Suggestion |
|---|---|---|
| Signature verification fails | Returns code=500 | Check signing plaintext, timestamp, private/public key match |
| Timestamp expired | Returns code=500 | Sync server time and retry |
| Nonce reused | Returns code=500 | Ensure unique random string; short-term dedup cache |
| Callback verification fails | Returns code=500 | Confirm latest Veloraya public key; check callback body unchanged |
Appendix
Appendix A: Key generation & management
Generate & store
Use 2048-bit RSA; public key in X.509 (DER) → Base64, private key in PKCS#8 (DER) → Base64.
Utility: output Base64 public/private keys
java
import java.security.*;
import java.util.Base64;
public class RsaGenerateKeyPair {
public static void main(String[] args) throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PublicKey publicKey = keyPair.getPublic();
byte[] publicKeyBytes = publicKey.getEncoded();
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes);
System.out.println(publicKeyBase64);
PrivateKey privateKey = keyPair.getPrivate();
byte[] privateKeyBytes = privateKey.getEncoded();
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
System.out.println(privateKeyBase64);
}
}Distribution & rotation
- Provide only the public key (Base64) to Veloraya; keep the private key yourself.
- Rotate every 6-12 months; keep old/new public keys active in parallel during migration.
Appendix B: Signing / verification helper code
java
public class VelorayaSignUtils {
/**
* Generate API request signature
*/
public static String generateApiSign(String merchantId, String timestamp,
String nonce, String body,
String merchantPrivateKeyBase64) {
try {
// 1. Build signing plaintext
String content = String.format("merchantId=%s,timestamp=%s,nonce=%s,body=%s",
merchantId, timestamp, nonce, body);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
// 2. Parse Base64-encoded merchant private key
byte[] keyBytes = Base64.getDecoder().decode(merchantPrivateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
// 3. Sign with SHA256withRSA
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(contentBytes);
return Base64.getEncoder().encodeToString(sig.sign());
} catch (Exception e) {
throw new RuntimeException("Failed to generate API request signature", e);
}
}
/**
* Verify callback signature
*/
public static boolean verifyCallbackSign(String receivedSign, String timestamp,
String nonce, String body,
String velorayaPublicKeyBase64) {
try {
// 1. Build callback verification plaintext
String content = String.format("timestamp=%s,nonce=%s,body=%s",
timestamp, nonce, body);
byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
// 2. Parse Base64-encoded Veloraya public key
byte[] keyBytes = Base64.getDecoder().decode(velorayaPublicKeyBase64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
// 3. Decode signature and verify with public key
byte[] signature = Base64.getDecoder().decode(receivedSign);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(contentBytes);
return sig.verify(signature);
} catch (Exception e) {
throw new RuntimeException("Callback verification failed", e);
}
}
}