Skip to content

API Signature Specification ​

Scope & Roles ​

  • Applicable APIs: Veloraya collection (Payin), payout, query, balance, bank list and all callback notifications for Ecuador.
  • 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 ​

  1. Generate the request body JSON (body) from business parameters.
  2. Generate timestamp and nonce, then build the signature plaintext per the spec.

Merchant signs and sends the request ​

  1. Sign the plaintext with the merchant private key + SHA256withRSA, Base64-encode it to get Signature.
  2. Send the request with headers MerchantId / Timestamp / Nonce / Signature and the body to Veloraya.

Veloraya verifies and processes ​

  1. Validate timestamp, nonce and signature.
  2. If valid, execute routing, risk control, accounting and return the synchronous response.

Veloraya sends callbacks ​

  1. When order status changes, Veloraya builds the callback body, generates timestamp/nonce, signs with the platform private key and sends it to the merchant.
  2. 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 ​

HeaderRequiredTypeDescription
Content-TypeYstringFixed to application/json
MerchantIdYstringMerchant ID assigned by Veloraya
TimestampYstringMillisecond timestamp; must be close to server time
NonceYstringUnique random string
SignatureYstringRequest 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, body in the signing plaintext is "".

Request Signing Guide (Merchant side) ​

Signing plaintext format ​

merchantId={merchantId},timestamp={timestamp},nonce={nonce},body={requestBodyJson}

Steps ​

  1. Build plaintext: concatenate per the format above.
  2. UTF-8 encode: convert the plaintext to UTF-8 bytes.
  3. Sign: use SHA256withRSA with the merchant private key.
  4. Base64 encode: encode the signature bytes to Base64 as the Signature header.

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 ​

  1. Read Signature, Timestamp, Nonce from the callback headers (callbacks do not contain merchantId).
  2. Build the plaintext timestamp={...},nonce={...},body={requestBodyJson}.
  3. 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 ​

ScenarioSymptomSuggestion
Signature verification failsReturns code=500Check signing plaintext, timestamp, private/public key match
Timestamp expiredReturns code=500Sync server time and retry
Nonce reusedReturns code=500Ensure unique random string; short-term dedup cache
Callback verification failsReturns code=500Confirm 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);
        }
    }
}

Released under the MIT License.