Skip to main content

Signature Algorithm

Sign every API request with RSA SHA256 to authenticate securely. All requests must be signed before they are sent.

How It Works​

▾
1Sort JSON
2Base64 Encode
3Build String
4Sign & Send

Required Headers​

▾

Add these to every signed request:

AuthorizationBearer <access_token>
X-Signaturesha256 <signature>
X-Nonce-Strrandom string
X-Timestampunix timestamp
Content-Typeapplication/json

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

important

Both the data object and every nested object must be sorted. Before encoding, replace special characters:

CharacterReplace With
<\u003c
>\u003e
&\u0026
note

The request body depends on which API endpoint you are calling. The body below is just an EXAMPLE.

Example Request (before sorting)
JSON
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}
Sorted + Compact
Text
{"additionalData":"world","amount":10,"currencyType":"MYR",...}

Request Body Parameters​

orderOBJECTrequired

Object of order

titleSTRINGrequired

Order title. Max 32 characters.

Example: "Sales"

detailSTRINGrequired

Order detail. Max 600 characters.

Example: "1 x iPhone X; 2 x SAMSUNG S8"

additionalDataSTRINGrequired

Additional order description.

Example: "Sales"

amountINTEGERrequired

Amount of order in cents. Minimum is RM 0.10 (amount: 10). Required only when isPrefillAmount = true.

Example: 100

currencyTypeSTRINGrequired

Currency notation. Currently only MYR is supported.

Example: "MYR"

idSTRING

Your internal order reference ID.

Example: "6170506694335521334"

customerOBJECTrequired

Object of customer

userIdSTRINGrequired

Required if tokenization is enabled.

Example: "13245876"

emailSTRING

Customer email address.

Example: ""

countryCodeSTRING

Customer country code.

Example: ""

phoneNumberSTRING

Customer phone number.

Example: ""

methodARRAYrequired

RM currently supported method

Example: []

typeOBJECTrequired

Object of type

typeSTRINGrequired

Use WEB_PAYMENT for browser-based payments or MOBILE_PAYMENT for mobile app payments.

Example: "WEB_PAYMENT"

storeIdSTRINGrequired

ID of the store to create QR code

Example: "10946114768247530"

redirectUrlSTRINGrequired

URL to redirect after payment is made

Example: "https://google.com"

notifyUrlSTRINGrequired

Webhook URL that RM will call with the payment result

Example: "https://google.com"

layoutVersionSTRING

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.

Base64 Encoded
Text
ewogICAgIm9yZGVyIjogewogICAgCSJ0aXRsZSI6ICJoZWxsbyIsCiAgICAJImRldGFpbCI6IC
IiLAogICAgCSJhZGRpdGlvbmFsRGF0YSI6ICJ3b3JsZCIsCgkgICAgImFtb3VudCI6IDEwLAoJIC
AgICJjdXJyZW5jeVR5cGUiOiAiTVlSIiwKCSAgICAiaWQiOiAgIjcyMTEiCiAgICB9LAogICAgIm
N1c3RvbWVyIjogewogICAgInVzZXJJZCI6ICIiLAogICAgImVtYWlsIjogIiIKfSwKICAgICJtZX
Rob2QiOltdLAogICAgInR5cGUiOiAiV0VCX1BBWU1FTlQiLAogICAgInN0b3JlSWQiOiAiMTYwOD
EyMzAzNTU2NDUzODEyMSIsCiAgICAicmVkaXJlY3RVcmwiOiAiaHR0cHM6Ly9yZXZlbnVlbW9uc3
Rlci5teSIsCiAgICAibm90aWZ5VXJsIjogImh0dHBzOi8vZGV2LXJtLWFwaS5hcC5uZ3Jvay5pby
IsCiAgICAibGF5b3V0VmVyc2lvbiI6InYzIgp9

Step 3 — Construct the Signing String​

▾

Combine the encoded body with the request parameters, joined by & in alphabetical order.

important
  • If the body is empty, the data parameter can be skipped.
  • If you are verifying a callback signature, the requestUrl can be skipped.
dataSTRINGrequired

Base64-encoded data body from Step 2.

Example: eyJhZGRpdGlvbmFs...

methodSTRINGrequired

HTTP call method, lowercase.

Example: "post"

nonceStrSTRINGrequired

A unique random string. Must not be reused within 120 seconds.

Example: "VYNknZohxwicZMaWbNdBKUrnrxDtaRhN"

requestUrlSTRINGrequired

The exact API URL being called, including the full path.

Example: "https://sb-open.revenuemonster.my/v3/payment/online"

signTypeSTRINGrequired

The signing algorithm. Always this value.

Example: "sha256"

timestampSTRINGrequired

UNIX timestamp (UTC). Must be within 120 seconds of server time.

Example: "1527407052"

Signing String
Text
data=eyJhZGRpdGlvbmFs...&method=post&nonceStr=VYNknZohxwicZMaWbNdBKUrnrxDtaRhN&requestUrl=https://sb-open.revenuemonster.my/v3/payment/online&signType=sha256&timestamp=1527407052

Step 4 — Sign with Your Private Key​

▾

Sign the signing string using RSA SHA256 with your private key. Base64-encode the result.

important

Make sure the matching public key has been uploaded to the RM Merchant Portal, or verification will fail.

Signature
Text
sha256 IrBg6t73VsH7ieEnQDB4CXHFjMWUkp8Dtddpxqw+4Gvz6Tag7Dx6nrfAt2ofYK8xZN9aBCvAKAfmAOGWIXnsTXfhFBnMA2kadiga7ufUJ81ozyhllbiliRM2ugw1OcqSTLRHWBPhrVwhHBxgDiG9wbuI3FKURrz+CufYYakFoCw=

Code Examples​

▾
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` +
`&timestamp=${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

Response​

▾
itemOBJECT

Response payload.

checkoutIdSTRING

Code to identify the web payment session.

Example: "1548316308361173347"

urlSTRING

Checkout URL. To change the base URL, replace the domain to your desired URL.

Example: "https://sb-pg.revenuemonster.my/checkout?checkoutId=1548316308361173347"

codeSTRING

SUCCESS if the call succeeded. Otherwise returns an error code object. See Appendix: Error Codes.

Example: "SUCCESS"

Example Response
JSON
1{
2"item": {
3 "checkoutId": "1548316308361173347",
4 "url": "https://sb-pg.revenuemonster.my/checkout?checkoutId=1548316308361173347"
5},
6"code": "SUCCESS"
7}

Common Mistakes​

▾
❌ JSON keys not sorted

Sort alphabetically including nested objects. Check every level.

❌ Extra spaces in JSON

Make it compact: {"a":1} not {"a": 1}. Remove all whitespace.

❌ Forgot to escape special characters

Replace < → \u003c, > → \u003e, & → \u0026 before encoding.

❌ Timestamp too old

Must be within 120 seconds of server UTC time. Sync your clock.

❌ Reused nonce

Generate a new nonce for each request. Don't reuse within 120 seconds.

❌ Wrong private key

Verify your private key matches the public key uploaded to the portal.


Security Best Practices​

▾
✅ DO
  • Store private keys securely
  • Use environment variables
  • Generate a unique nonce per request
  • Verify certificate validity
  • Rotate keys periodically
❌ DON'T
  • 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:

info

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.

note

Compare the preVerifyContent steps below with your own output to find where they diverge.

Debug Response
JSON
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&timestamp=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&timestamp=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​

▾
CauseFix
Private key doesn't match the public key uploaded to the Merchant PortalRe-upload the correct public key
JSON keys not sorted alphabeticallyCheck nested objects too
JSON body still contains spaces or newlines before Base64 encodingMake the JSON compact
Special characters (<, >, &) not replaced before encodingReplace with \u003c, \u003e, \u0026
X-Timestamp is more than 120 seconds away from server UTC timeSync your system clock
X-Nonce-Str was reused within a 120-second windowGenerate a new nonce for each request
Amount is in cents — "amount": 100 = RM 1.00Multiply the amount by 100

Need Help?​

▾