Request Signature and Verification Guide

To ensure the authenticity and integrity of requests during transmission, RedotPay uses asymmetric encryption algorithms for digital signatures on API requests. This guide details how to generate signatures for API requests sent to RedotPay and how to verify signatures for callback notifications received from RedotPay.

Signature Scope

To make integration and debugging easier, RSA signature verification is currently turned off in the Sandbox environment. You can call Sandbox Open APIs without computing a valid sign header.

In Production and Gray environments, all API requests containing body parameters must be signed using the SHA256withRSA algorithm. Signature information is transmitted through HTTP request headers.

Key Management

Key Generation and Configuration

  1. Generate Key Pair: Use OpenSSL or other tools to generate an RSA key pair (recommended 2048-bit)

    # Generate private key
    openssl genrsa -out private_key.pem 2048
    
    # Generate public key from private key
    openssl rsa -in private_key.pem -pubout -out public_key.pem
    
  2. Configure Keys:

    • Upload your public key to the RedotPay merchant platform
    • Obtain RedotPay's public key from the RedotPay platform for callback signature verification
    • The private key must be securely stored on your server and never leaked

Key Version Management

Use the X-R-Key-Version header to specify the key version, using natural numbers (e.g., 1, 2, 3...):

  • 1: Initial key version
  • 2: Second rotation key (when updating keys)
  • 3: Third rotation key
  • And so on...

RedotPay Platform Public Keys

Use these keys to verify webhook / callback signatures (X-R-Signature). Select the key by environment and X-R-Key-Version. Do not use the Sandbox key in Production, or the Production key in Sandbox.

EnvironmentX-R-Key-VersionPublic key
Sandbox1PEM below
Production1PEM below

Sandbox public key (X-R-Key-Version: 1)

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuctrVK3eP8hpoJf7FMet
lcR77FYcj9HtrkySyGDRt5HHwdwgM8jK0kfE4ag/zI8goe8M0iJ2o7n3VCfTzn8O
yfU0bu6KzDti1WOJV9fv4XtSmhm9W4WKjIc8uDQViR7E8trzcrbKFVbKVGng1+z0
KobQBDtWhjUeXKktUq1lpiejTS+XjXej26ANPfwbqbY+/6kBB3sWbt9BLDI/WhPY
XnFV9oJWod9I/dYUgUUA/b/+bI1wlobNntBDxiNmX0kbqpGZbzO6l9wWFXZiFCD2
5QtBOZlMbn9noH4KW3DnKGc2nKNz/f2FEM9DJKn3P7NGFVy6O/Q5NzcbFs+DI6nT
ywIDAQAB
-----END PUBLIC KEY-----

Production public key (X-R-Key-Version: 1)

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMn4r06M/cp2amkbCxIs
PSr030JoCFeymwjTZrBnI8kW4mtL6JtUPYpJTFgCB8ZQoV75lEmUw8gSLbN770Cc
5EOi1dF4ekmLQ7Ez0SFUbQgJa7Vg5wBdSKcbUmkKGviJt+iZRJ0tZsPpXMPqIo9Y
OWJagfPbDhEwT2t1ANP4ou98sCqLqELI80iYm8+W4B9IvBW4lc+H5BAPtXpYMtlZ
6stCnvHXd1EjvlTak25v5xJ8AInEeAy8/D2glunmz/VfPyoB5OHPgnYVU66HyeQc
O1ZY/jzB5d6I/zX4JENG1xrP8ThPZ9qMWtmputJ0XYKymiZgZP6vh0L+G6P/Z98v
lQIDAQAB
-----END PUBLIC KEY-----

When RedotPay rotates keys, a new X-R-Key-Version and public key will be published before the old key is retired. Always select the public key by environment and X-R-Key-Version.

Generating Request Signatures

Step 1: Prepare Signature Elements

  1. Determine the key version and load the corresponding private key
  2. Prepare the following signature elements:
    • http-method: HTTP request method (e.g., POST, must be uppercase)
    • http-uri: API URI path (e.g., /openapi/v2/order/create)
    • appKey: Your merchant identifier
    • timestamp: Current Unix timestamp (milliseconds)
    • requestBody: JSON format request body string (compact format, no extra spaces)

Step 2: Construct the String to Sign

String to sign (stringToSign) format:

{http-method} {http-uri}\n{appKey}.{timestamp}.{requestBody}

Note: There is a space between {http-method} and {http-uri}.

Example:

POST /openapi/v2/order/create
4CA7B705-8EF5-4AC3-A0B6-9A4B84EF13B6.1763555087656.{"amount":100,"currency":"USD"}

Step 3: Generate Signature Using SHA256withRSA

Use your RSA private key to sign stringToSign, then perform Base64 encoding.

Signature generation formula:

signature = base64Encode(sha256withRSA(stringToSign, privateKey))

Java Code Example:

import java.security.*;
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class RsaSignatureGenerator {
    public static String generateSignature(String httpMethod, String httpUri,
                                          String appKey, PrivateKey privateKey,
                                          long timestamp, String requestBody) throws Exception {
        // 1. Construct the string to sign
        String stringToSign = String.format("%s %s\n%s.%d.%s",
                httpMethod, httpUri, appKey, timestamp, requestBody);
        
        // 2. Sign using SHA256withRSA
        Signature privateSignature = Signature.getInstance("SHA256withRSA");
        privateSignature.initSign(privateKey);
        privateSignature.update(stringToSign.getBytes(StandardCharsets.UTF_8));
        byte[] signatureBytes = privateSignature.sign();
        
        // 3. Base64 encode
        String base64Signature = Base64.getEncoder().encodeToString(signatureBytes);
        return base64Signature;
    }
    
    // Load private key
    public static PrivateKey loadPrivateKey(String keyPath) throws Exception {
        String privateKeyPEM = new String(java.nio.file.Files.readAllBytes(
                java.nio.file.Paths.get(keyPath)));
        privateKeyPEM = privateKeyPEM
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s", "");
        
        byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decoded));
    }
}

Python Code Example:

import base64
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import time

def generate_signature(http_method: str, http_uri: str,
                      app_key: str, private_key_path: str,
                      request_body: str) -> tuple:
    """
    Generate RSA signature
    
    :return: (timestamp, signature, key_version) tuple
    """
    # 1. Get timestamp and key version
    timestamp = int(time.time() * 1000)
    key_version = 1  # Adjust according to the actual key version used
    
    # 2. Construct the string to sign
    string_to_sign = f"{http_method} {http_uri}\n{app_key}.{timestamp}.{request_body}"
    
    # 3. Load private key
    with open(private_key_path, "rb") as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None,
            backend=default_backend()
        )
    
    # 4. Sign using SHA256
    signature_bytes = private_key.sign(
        string_to_sign.encode('utf-8'),
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    
    # 5. Base64 encode
    return base64.b64encode(signature_bytes).decode('utf-8')

Node.js Code Example:

const crypto = require('crypto');
const fs = require('fs');

async function generateSignature(httpMethod, httpUri, appKey, privateKeyPath, requestBody) {
    // 1. Get timestamp and key version
    const timestamp = Date.now();
    const keyVersion = 1; // Adjust according to the actual key version used

    // 2. Construct the string to sign
    const stringToSign = `${httpMethod} ${httpUri}\n${appKey}.${timestamp}.${requestBody}`;

    // 3. Load private key
    const privateKeyPem = fs.readFileSync(privateKeyPath, 'utf8');

    // 4. Sign using SHA256withRSA
    const signer = crypto.createSign('RSA-SHA256');
    signer.update(stringToSign, 'utf8');
    const signatureBytes = signer.sign(privateKeyPem);

    // 5. Base64 encode (raw Base64, do NOT URL-encode)
    const signature = signatureBytes.toString('base64');
    return { timestamp, signature, keyVersion };
}

Step 4: Send the Request with Signature

Add the generated signature to the HTTP request headers:

Complete Request Headers Example:

POST /openapi/v2/order/create HTTP/1.1
Host: api.redotpay.com
Content-Type: application/json
X-Merchant-Ak: 4CA7B705-8EF5-4AC3-A0B6-9A4B84EF13B6
X-R-Ts: 1763555087656
X-R-Key-Version: 1
X-R-Signature: Base64 signature string

{"amount":100,"currency":"USD"}

Verifying RedotPay Callback Notification Signatures

Callback Notification Signature Rules

Callback notification string to sign format:

{appKey}.{timestamp}.{requestBody}

Note: Callback notification signatures do not include http-method and http-uri.

Verification Steps

  1. Extract elements: From callback request headers:

    • X-R-Ts: Timestamp
    • X-R-Signature: Base64 signature
    • X-R-Key-Version: Key version (natural number)
  2. Decode signature: Base64 decode the signature

  3. Construct verification string: Format as {appKey}.{timestamp}.{requestBody}

  4. Obtain RedotPay public key: Get the corresponding RedotPay public key for your environment and X-R-Key-Version from RedotPay Platform Public Keys

  5. Verify signature: Use RedotPay public key to verify the signature

Java Verification Example:

import java.security.*;
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class CallbackVerifier {
    public static boolean verifyCallback(String appKey, String timestamp,
                                        String requestBody, String encodedSignature,
                                        int keyVersion, PublicKey redotpayPublicKey) throws Exception {
        // 1. Decode signature
        byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
        
        // 2. Construct verification string
        String stringToVerify = String.format("%s.%s.%s", appKey, timestamp, requestBody);
        
        // 3. Verify signature
        Signature publicSignature = Signature.getInstance("SHA256withRSA");
        publicSignature.initVerify(redotpayPublicKey);
        publicSignature.update(stringToVerify.getBytes(StandardCharsets.UTF_8));
        
        return publicSignature.verify(signatureBytes);
    }
    
    // Load RedotPay public key
    public static PublicKey loadRedotpayPublicKey(int keyVersion) throws Exception {
        // Get corresponding RedotPay public key based on keyVersion
        // This should obtain the public key string from secure storage or configuration
        String publicKeyPEM = getPublicKeyByVersion(keyVersion);
        
        publicKeyPEM = publicKeyPEM
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");
        
        byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(new X509EncodedKeySpec(decoded));
    }
    
    // Get public key by version
    private static String getPublicKeyByVersion(int version) {
        switch (version) {
            case 1:
                return "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."; // Version 1 public key
            case 2:
                return "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."; // Version 2 public key
            default:
                throw new IllegalArgumentException("Unsupported key version: " + version);
        }
    }
}

Python Verification Example:

import base64
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend

def verify_callback_signature(headers: dict, request_body: str, 
                            app_key: str, expected_key_version: int) -> bool:
    """
    Verify callback signature
    
    :param headers: Request headers containing X-R-Ts, X-R-Signature, X-R-Key-Version
    :param request_body: Original request body
    :param app_key: Merchant appKey
    :param expected_key_version: Expected key version (natural number)
    :return: Verification result
    """
    try:
        # 1. Extract parameters from headers
        timestamp = headers.get('X-R-Ts')
        encoded_signature = headers.get('X-R-Signature')
        received_key_version = headers.get('X-R-Key-Version')
        
        # 2. Check key version
        if not received_key_version or int(received_key_version) != expected_key_version:
            return False
            
        # 3. Get corresponding RedotPay public key
        public_key = get_redotpay_public_key(int(received_key_version))
        
        # 4. Decode signature
        signature_bytes = base64.b64decode(encoded_signature)
        
        # 5. Construct verification string
        string_to_verify = f"{app_key}.{timestamp}.{request_body}"
        
        # 6. Verify signature
        public_key.verify(
            signature_bytes,
            string_to_verify.encode('utf-8'),
            padding.PKCS1v15(),
            hashes.SHA256()
        )
        return True
        
    except (InvalidSignature, ValueError, KeyError, TypeError):
        return False

def get_redotpay_public_key(key_version: int):
    """
    Get RedotPay public key based on key version
    
    :param key_version: Key version (natural number)
    """
    # Get corresponding public key file path based on version number
    key_file_path = f"redotpay_public_key_v{key_version}.pem"
    
    with open(key_file_path, "rb") as key_file:
        return serialization.load_pem_public_key(
            key_file.read(),
            backend=default_backend()
        )

Node.js Verification Example:

const crypto = require('crypto');
const fs = require('fs');

function verifyCallback(appKey, timestamp, requestBody, signature, keyVersion, redotpayPublicKey) {
    try {
        // 1. Base64 decode the signature (raw Base64, do NOT URL-decode)
        const signatureBytes = Buffer.from(signature, 'base64');

        // 2. Construct the verification string
        const stringToVerify = `${appKey}.${timestamp}.${requestBody}`;

        // 3. Verify signature
        const verifier = crypto.createVerify('RSA-SHA256');
        verifier.update(stringToVerify, 'utf8');
        return verifier.verify(redotpayPublicKey, signatureBytes);
    } catch (e) {
        return false;
    }
}

// Load RedotPay public key
function loadRedotpayPublicKey(keyVersion) {
    const publicKeyPem = getPublicKeyByVersion(keyVersion);
    return crypto.createPublicKey(publicKeyPem);
}

// Get public key by version
function getPublicKeyByVersion(version) {
    switch (version) {
        case 1:
            return `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... // Version 1 public key
-----END PUBLIC KEY-----`;
        default:
            throw new Error(`Unsupported key version: ${version}`);
    }
}

Key Considerations

1. Algorithm Requirements

  • Must use SHA256withRSA algorithm
  • Recommended key length 2048-bit or higher
  • Perform SHA256 hash calculation before signing

2. Key Security Management

  • Private key: Must be securely stored on the server, recommended to use HSM (Hardware Security Module) or key management service
  • Public key exchange:
    • Upload your public key to the RedotPay merchant platform
    • Obtain and securely store RedotPay's public key from the RedotPay platform
  • Key rotation:
    • Regularly rotate keys (recommended every 12 months)
    • Upload new public key first, then update the key version number in code during rotation
    • Keep old keys available for a period to ensure smooth transition

3. Timestamp Verification

  • Timestamp deviation should be within ±5 minutes
  • Prevent replay attacks by recording processed timestamps

4. Error Handling

try {
    // Signature or verification logic
} catch (InvalidKeyException e) {
    // Invalid key, check key format or version
} catch (SignatureException e) {
    // Signature verification failed
} catch (InvalidAlgorithmParameterException e) {
    // Algorithm parameter error
} catch (NumberFormatException e) {
    // Key version number format error, cannot convert to natural number
}

Frequently Asked Questions

Q1: Possible reasons for signature verification failure?

  • Incorrect string to sign format (check line breaks and punctuation)
  • Timestamp outside allowed range
  • Key version mismatch
  • Public/private key mismatch
  • Request body JSON format changes (spaces, line breaks, etc.)

Q2: How to test signature implementation?

  1. Use test environment key pairs
  2. Record generated string to sign
  3. Verify signature using OpenSSL command line
  4. Compare with RedotPay's example code

Q3: Precautions during key rotation?

  1. Ensure both parties have completed configuration before new key takes effect
  2. Update key version number in code (natural number increment)
  3. Monitor error rates during old and new key transition period
  4. Keep old keys available for at least 7 days before disabling

Signature Example Checklist

Ensure your implementation includes all the following elements:

  • Use correct string to sign format
  • Include X-R-Key-Version request header with natural number value
  • Use SHA256withRSA algorithm
  • Base64 encode the signature
  • Use correct string format for callback verification (without http-method and uri)
  • Use millisecond Unix timestamp
  • Use compact JSON format for request body
  • Use natural numbers for key version (1, 2, 3...)

By following this guide, you can ensure secure and reliable integration with RedotPay API. For any issues, please refer to the latest documentation on the RedotPay merchant platform or contact technical support.



Did this page help you?