Signature Algorithm
Sign every API request with RSA SHA256 to authenticate securely. All requests must be signed before they are sent.
How It Works
▾
Required Headers
▾
Add these to every signed request:
Step by Step
▾
Step 1 — Sort JSON Alphabetically
▾
Sort all keys alphabetically, including nested objects, then remove all spaces and newlines so the JSON is compact.
Method: POST
Both the data object and every nested object must be sorted. Before encoding, replace special characters:
| Character | Replace With |
|---|---|
< | \u003c |
> | \u003e |
& | \u0026 |
The request body depends on which API endpoint you are calling. The body below is just an EXAMPLE.
1{2"order": {3 "title": "hello",4 "detail": "",5 "additionalData": "world",6 "amount": 10,7 "currencyType": "MYR",8 "id": "7211"9},10"customer": {11 "userId": "13245876",12 "email": ""13},14"method": [],15"type": "WEB_PAYMENT",16"storeId": "1608123035564538121",17"redirectUrl": "https://revenuemonster.my",18"notifyUrl": "https://dev-rm-api.ap.ngrok.io",19"layoutVersion": "v3"20}
{"additionalData":"world","amount":10,"currencyType":"MYR",...}
Request Body Parameters
Object of order
Order title. Max 32 characters.
Example: "Sales"
Order detail. Max 600 characters.
Example: "1 x iPhone X; 2 x SAMSUNG S8"
Additional order description.
Example: "Sales"
Amount of order in cents. Minimum is RM 0.10 (amount: 10). Required only when isPrefillAmount = true.
Example: 100
Currency notation. Currently only MYR is supported.
Example: "MYR"
Your internal order reference ID.
Example: "6170506694335521334"
Object of customer
Required if tokenization is enabled.
Example: "13245876"
Customer email address.
Example: ""
Customer country code.
Example: ""
Customer phone number.
Example: ""
RM currently supported method
Example: []
Object of type
Use WEB_PAYMENT for browser-based payments or MOBILE_PAYMENT for mobile app payments.
Example: "WEB_PAYMENT"
ID of the store to create QR code
Example: "10946114768247530"
URL to redirect after payment is made
Example: "https://google.com"
Webhook URL that RM will call with the payment result
Example: "https://google.com"
Select layout for Web payment. v1 / v2 (Credit Card) / v3 (Credit Card and FPX)
Example: "v3"
Step 2 — Base64 Encode
▾
Encode the compact, sorted JSON string using Base64.
ewogICAgIm9yZGVyIjogewogICAgCSJ0aXRsZSI6ICJoZWxsbyIsCiAgICAJImRldGFpbCI6ICIiLAogICAgCSJhZGRpdGlvbmFsRGF0YSI6ICJ3b3JsZCIsCgkgICAgImFtb3VudCI6IDEwLAoJICAgICJjdXJyZW5jeVR5cGUiOiAiTVlSIiwKCSAgICAiaWQiOiAgIjcyMTEiCiAgICB9LAogICAgImN1c3RvbWVyIjogewogICAgInVzZXJJZCI6ICIiLAogICAgImVtYWlsIjogIiIKfSwKICAgICJtZXRob2QiOltdLAogICAgInR5cGUiOiAiV0VCX1BBWU1FTlQiLAogICAgInN0b3JlSWQiOiAiMTYwODEyMzAzNTU2NDUzODEyMSIsCiAgICAicmVkaXJlY3RVcmwiOiAiaHR0cHM6Ly9yZXZlbnVlbW9uc3Rlci5teSIsCiAgICAibm90aWZ5VXJsIjogImh0dHBzOi8vZGV2LXJtLWFwaS5hcC5uZ3Jvay5pbyIsCiAgICAibGF5b3V0VmVyc2lvbiI6InYzIgp9
Step 3 — Construct the Signing String
▾
Combine the encoded body with the request parameters, joined by & in alphabetical order.
- If the body is empty, the
dataparameter can be skipped. - If you are verifying a callback signature, the
requestUrlcan be skipped.
Base64-encoded data body from Step 2.
Example: eyJhZGRpdGlvbmFs...
HTTP call method, lowercase.
Example: "post"
A unique random string. Must not be reused within 120 seconds.
Example: "VYNknZohxwicZMaWbNdBKUrnrxDtaRhN"
The exact API URL being called, including the full path.
Example: "https://sb-open.revenuemonster.my/v3/payment/online"
The signing algorithm. Always this value.
Example: "sha256"
UNIX timestamp (UTC). Must be within 120 seconds of server time.
Example: "1527407052"
data=eyJhZGRpdGlvbmFs...&method=post&nonceStr=VYNknZohxwicZMaWbNdBKUrnrxDtaRhN&requestUrl=https://sb-open.revenuemonster.my/v3/payment/online&signType=sha256×tamp=1527407052
Step 4 — Sign with Your Private Key
▾
Sign the signing string using RSA SHA256 with your private key. Base64-encode the result.
Make sure the matching public key has been uploaded to the RM Merchant Portal, or verification will fail.
sha256 IrBg6t73VsH7ieEnQDB4CXHFjMWUkp8Dtddpxqw+4Gvz6Tag7Dx6nrfAt2ofYK8xZN9aBCvAKAfmAOGWIXnsTXfhFBnMA2kadiga7ufUJ81ozyhllbiliRM2ugw1OcqSTLRHWBPhrVwhHBxgDiG9wbuI3FKURrz+CufYYakFoCw=
Code Examples
▾
- Node.js
- Python
- Go
- cURL
const crypto = require('crypto');
const fs = require('fs');
// Load your RSA private key from a secure location — never hardcode it.
const privateKey = fs.readFileSync(process.env.RM_PRIVATE_KEY_PATH, 'utf8');
const url = 'https://sb-open.revenuemonster.my/v3/payment/online';
const body = {
order: {
title: 'hello',
detail: '',
additionalData: 'world',
amount: 10,
currencyType: 'MYR',
id: '7211',
},
customer: {
userId: '13245876',
email: '',
},
method: [],
type: 'WEB_PAYMENT',
storeId: '1608123035564538121',
redirectUrl: 'https://revenuemonster.my',
notifyUrl: 'https://dev-rm-api.ap.ngrok.io',
layoutVersion: 'v3',
};
// 1. Sort keys recursively, then compact and escape special characters.
function sortObject(value) {
if (Array.isArray(value)) {
return value.map(sortObject);
}
if (value && typeof value === 'object') {
return Object.keys(value)
.sort()
.reduce((acc, key) => {
acc[key] = sortObject(value[key]);
return acc;
}, {});
}
return value;
}
const compact = JSON.stringify(sortObject(body))
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
// 2. Base64-encode the compact JSON.
const data = Buffer.from(compact).toString('base64');
// 3. Build the signing string (parameters in alphabetical order).
const nonceStr = crypto.randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000).toString();
const signString =
`data=${data}` +
`&method=post` +
`&nonceStr=${nonceStr}` +
`&requestUrl=${url}` +
`&signType=sha256` +
`×tamp=${timestamp}`;
// 4. Sign with RSA-SHA256, then Base64-encode the signature.
const signature = crypto
.createSign('RSA-SHA256')
.update(signString)
.sign(privateKey, 'base64');
// Send with headers:
// X-Signature: `sha256 ${signature}`
// X-Nonce-Str: nonceStr
// X-Timestamp: timestamp
import base64
import json
import os
import secrets
import time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# Load your RSA private key from a secure location — never hardcode it.
with open(os.environ["RM_PRIVATE_KEY_PATH"], "rb") as key_file:
private_key = serialization.load_pem_private_key(key_file.read(), password=None)
url = "https://sb-open.revenuemonster.my/v3/payment/online"
body = {
"order": {
"title": "hello",
"detail": "",
"additionalData": "world",
"amount": 10,
"currencyType": "MYR",
"id": "7211",
},
"customer": {
"userId": "13245876",
"email": "",
},
"method": [],
"type": "WEB_PAYMENT",
"storeId": "1608123035564538121",
"redirectUrl": "https://revenuemonster.my",
"notifyUrl": "https://dev-rm-api.ap.ngrok.io",
"layoutVersion": "v3",
}
def sort_object(value):
if isinstance(value, dict):
return {key: sort_object(value[key]) for key in sorted(value)}
if isinstance(value, list):
return [sort_object(item) for item in value]
return value
# 1. Sort keys recursively, then compact and escape special characters.
compact = json.dumps(sort_object(body), separators=(",", ":"), ensure_ascii=False)
compact = (
compact.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("&", "\\u0026")
)
# 2. Base64-encode the compact JSON.
data = base64.b64encode(compact.encode()).decode()
# 3. Build the signing string (parameters in alphabetical order).
nonce_str = secrets.token_hex(16)
timestamp = str(int(time.time()))
sign_string = (
f"data={data}"
f"&method=post"
f"&nonceStr={nonce_str}"
f"&requestUrl={url}"
f"&signType=sha256"
f"×tamp={timestamp}"
)
# 4. Sign with RSA-SHA256, then Base64-encode the signature.
signature = base64.b64encode(
private_key.sign(sign_string.encode(), padding.PKCS1v15(), hashes.SHA256())
).decode()
# Send header -> X-Signature: f"sha256 {signature}"
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"time"
)
func main() {
// Load your RSA private key from a secure location — never hardcode it.
keyPEM, _ := os.ReadFile(os.Getenv("RM_PRIVATE_KEY_PATH"))
block, _ := pem.Decode(keyPEM)
privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
url := "https://sb-open.revenuemonster.my/v3/payment/online"
body := map[string]any{
"order": map[string]any{
"title": "hello",
"detail": "",
"additionalData": "world",
"amount": 10,
"currencyType": "MYR",
"id": "7211",
},
"customer": map[string]any{
"userId": "13245876",
"email": "",
},
"method": []string{},
"type": "WEB_PAYMENT",
"storeId": "1608123035564538121",
"redirectUrl": "https://revenuemonster.my",
"notifyUrl": "https://dev-rm-api.ap.ngrok.io",
"layoutVersion": "v3",
}
// 1. Marshal — Go sorts map keys and escapes <, >, & to \u003c automatically.
raw, _ := json.Marshal(body)
compact := string(raw)
// 2. Base64-encode the compact JSON.
data := base64.StdEncoding.EncodeToString([]byte(compact))
// 3. Build the signing string (parameters in alphabetical order).
nonce := make([]byte, 16)
rand.Read(nonce)
nonceStr := fmt.Sprintf("%x", nonce)
timestamp := fmt.Sprintf("%d", time.Now().Unix())
signString := fmt.Sprintf(
"data=%s&method=post&nonceStr=%s&requestUrl=%s&signType=sha256×tamp=%s",
data, nonceStr, url, timestamp,
)
// 4. Sign with RSA-SHA256, then Base64-encode the signature.
hashed := sha256.Sum256([]byte(signString))
sig, _ := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
signature := base64.StdEncoding.EncodeToString(sig)
fmt.Println("X-Signature: sha256", signature)
fmt.Println("X-Nonce-Str:", nonceStr)
fmt.Println("X-Timestamp:", timestamp)
}
curl --request POST \
--url 'https://sb-open.revenuemonster.my/v3/payment/online' \
--header 'Authorization: Bearer eyJhbGci...' \
--header 'Content-Type: application/json' \
--header 'X-Nonce-Str: VYNknZohxwicZMaWbNdBKUrnrxDtaRhN' \
--header 'X-Signature: sha256 IrBg6t73VsH7ieEnQDB4...' \
--header 'X-Timestamp: 1527407052' \
--data '{
"order": {
"title": "hello",
"detail": "",
"additionalData": "world",
"amount": 10,
"currencyType": "MYR",
"id": "7211"
},
"customer": {
"userId": "13245876",
"email": ""
},
"method": [],
"type": "WEB_PAYMENT",
"storeId": "1608123035564538121",
"redirectUrl": "https://revenuemonster.my",
"notifyUrl": "https://dev-rm-api.ap.ngrok.io",
"layoutVersion": "v3"
}'
Response
▾
Response payload.
Code to identify the web payment session.
Example: "1548316308361173347"
Checkout URL. To change the base URL, replace the domain to your desired URL.
Example: "https://sb-pg.revenuemonster.my/checkout?checkoutId=1548316308361173347"
SUCCESS if the call succeeded. Otherwise returns an error code object. See Appendix: Error Codes.
Example: "SUCCESS"
1{2"item": {3 "checkoutId": "1548316308361173347",4 "url": "https://sb-pg.revenuemonster.my/checkout?checkoutId=1548316308361173347"5},6"code": "SUCCESS"7}
Common Mistakes
▾
Sort alphabetically including nested objects. Check every level.
Make it compact: {"a":1} not {"a": 1}. Remove all whitespace.
Replace < → \u003c, > → \u003e, & → \u0026 before encoding.
Must be within 120 seconds of server UTC time. Sync your clock.
Generate a new nonce for each request. Don't reuse within 120 seconds.
Verify your private key matches the public key uploaded to the portal.
Security Best Practices
▾
- Store private keys securely
- Use environment variables
- Generate a unique nonce per request
- Verify certificate validity
- Rotate keys periodically
- Hardcode private keys
- Commit keys to git
- Reuse nonce values
- Skip the timestamp check
- Share private keys
Prerequisites — App & RSA Keys
▾
Signing requires an application and an RSA key pair from the Merchant Portal. If you haven't set those up yet:
See Set Up Your Application — create an app, obtain your clientId / clientSecret, generate your RSA keys, and (optionally) use the Signature Debugger.
Troubleshooting — INVALID_REQUEST_SIGNATURE
▾
If you receive an INVALID_REQUEST_SIGNATURE error, the response includes a debug object showing exactly what the server computed at each step.
Compare the preVerifyContent steps below with your own output to find where they diverge.
1{2"debug": {3 "preVerifyContent": {4 "step1": {5 "content": "{\"layoutVersion\":\"v2\",\"method\":[\"GOBIZ_MY\"],\"notifyUrl\":\"https://dev-rm-api.ap.ngrok.io\",\"order\":{\"additionalData\":\"world\",\"amount\":10,\"currencyType\":\"MYR\",\"detail\":\"hello\",\"id\":\"721115\",\"title\":\"hello\"},\"redirectUrl\":\"https://revenuemonster.my\",\"storeId\":\"10946114768247530\",\"type\":\"WEB_PAYMENT\"}",6 "remark": "Sort the json key alphabetically"7 },8 "step2": {9 "content": "eyJsYXlvdXRWZXJzaW9uIjoidjIiLCJtZXRob2QiOlsiR09CSVpfTVkiXSwibm90aWZ5VXJsIjoiaHR0cHM6Ly9kZXYtcm0tYXBpLmFwLm5ncm9rLmlvIiwib3JkZXIiOnsiYWRkaXRpb25hbERhdGEiOiJ3b3JsZCIsImFtb3VudCI6MTAsImN1cnJlbmN5VHlwZSI6Ik1ZUiIsImRldGFpbCI6ImhlbGxvIiwiaWQiOiI3MjExMTUiLCJ0aXRsZSI6ImhlbGxvIn0sInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9yZXZlbnVlbW9uc3Rlci5teSIsInN0b3JlSWQiOiIxMDk0NjExNDc2ODI0NzUzMCIsInR5cGUiOiJXRUJfUEFZTUVOVCJ9",10 "remark": "Encode the data using Base64 format"11 },12 "step3": {13 "content": "data=eyJsYXlvd...&method=post&nonceStr=XAYZRZNLGCKSTURRFKBIGYALUKLCLJOG&requestUrl=https://sb-open.revenuemonster.my/v3/payment/online&signType=sha256×tamp=1599467903",14 "remark": "Construct plain text parameters on this format. If the body is empty, the data parameter can be omitted."15 },16 "step4": {17 "content": "data=eyJsYXlvd...&method=post&nonceStr=XAYZRZNLGCKSTURRFKBIGYALUKLCLJOG&requestUrl=https://sb-open.revenuemonster.my/v3/payment/online&signType=sha256×tamp=1599467903",18 "remark": "Sign this content using sha256 with rsa private key and make sure the public key has been uploaded to the portal."19 },20 "step5": {21 "remark": "Pass the generated signature in the X-Signature header, prefixed with the sign type. Example: sha256 {{ signatureContent }}"22 }23 },24 "requestHeader": {25 "X-Nonce-Str": {26 "currentValue": "XAYZRZNLGCKSTURRFKBIGYALUKLCLJOG",27 "isValid": true,28 "remark": "The nonce string must not contain spaces and must be unique for at least 120 seconds."29 },30 "X-Signature": {31 "currentValue": "sha256 XvedDW8H2gqGL5gMzTHqDy1PXX3OqRF09WuQDkeCDwuinOAsPstcPOSefUwkyHPM9WPNKKHyR5qXbKNLC7UgQyGi8Ynio03kDo0p+g3BqXaUT1tpo5D8kv42Kh2S8CW4RkX2Dkf+Yxi2XMQ8l3kzPZaRyhudaGerUZony4Npzf63p4+oTBbXE01uX/4x/WL57+zkaaVRc1KlJsLdGsBmLlPOHLana7udJffJyxXhOmyokBuJ4GoOC8JpDG9oaKCNMZ88ow9CWWB0yRPrK2KeaEDwzCm2Jh8IFKw1gS6avQAwsjychZWv5XmAXkZ8ZQrnLXJquA09QpLxPTtOeQC9SA==",32 "isValid": false,33 "remark": "The signature is invalid."34 },35 "X-Timestamp": {36 "currentValue": "1599467903",37 "isValid": false,38 "remark": "The timestamp must be in UTC and within 120 seconds of the server time."39 }40 }41},42"error": {43 "code": "INVALID_REQUEST_SIGNATURE",44 "message": "The request signature is invalid"45}46}
Common Causes
▾
| Cause | Fix |
|---|---|
| Private key doesn't match the public key uploaded to the Merchant Portal | Re-upload the correct public key |
| JSON keys not sorted alphabetically | Check nested objects too |
| JSON body still contains spaces or newlines before Base64 encoding | Make the JSON compact |
Special characters (<, >, &) not replaced before encoding | Replace with \u003c, \u003e, \u0026 |
X-Timestamp is more than 120 seconds away from server UTC time | Sync your system clock |
X-Nonce-Str was reused within a 120-second window | Generate a new nonce for each request |
Amount is in cents — "amount": 100 = RM 1.00 | Multiply the amount by 100 |