Backend Setup
The SDK requires a server-side endpoint to generate payment signatures. Signatures ensure that payment requests are authentic and have not been tampered with.
Never generate signatures on the client side. The merchant token must remain secret on your backend.
Signature Scheme
The signature is composed of two SHA-256 hashes joined by ///:
part1 = SHA256( clientTimeStamp + sid + amount + referenceNumber )
part2 = SHA256( clientTimeStamp + merchantToken + accountNumber + amount + referenceNumber )
signature = part1 + "///" + part2
| Field | Description |
|---|---|
clientTimeStamp |
Epoch timestamp from the client (Date.now()) |
sid |
Session ID (UUID v4) generated by the client |
amount |
Payment amount as string (e.g., "100.00") |
referenceNumber |
Order/reference ID (empty string if not provided) |
merchantToken |
Your secret merchant token (server-side only) |
accountNumber |
Your merchant account number |
Why amount must be a string: The two hash inputs are built by concatenating clientTimeStamp, sid, amount, and referenceNumber as text. The signature only matches if the exact same character sequence is used on the server as in the SDK. Numeric types can change how a value is rendered (for example 100 versus 100.00, or different fractional precision), which would change the hash. Passing amount as a string end-to-end keeps one fixed representation—including the decimal places and padding you intend—so the backend signature and the client payment request stay aligned.
Node.js (signature steps)
Below is a minimal illustration of the hashing and concatenation only. Your service should still validate amount, accountNumber, sid, and clientTimeStamp from the client before calling this.
const crypto = require('crypto');
const MERCHANT_TOKEN = process.env.MERCHANT_TOKEN || 'your-merchant-token';
function hash256(message) {
return crypto.createHash('sha256').update(message).digest('hex');
}
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function calculateSignature(clientTimeStamp, sid, amount, referenceNumber, accountNumber, merchantToken) {
if (typeof sid !== 'string' || !UUID_V4_REGEX.test(sid)) {
throw new Error('sid must be a valid GUID (UUID v4)');
}
const amountPart = (amount == null || amount === '' || amount === 'undefined') ? '' : String(amount);
const token = merchantToken || MERCHANT_TOKEN;
const ref = referenceNumber || '';
const part1 = hash256(`${clientTimeStamp}${sid}${amountPart}${ref}`);
const part2 = hash256(`${clientTimeStamp}${token}${accountNumber}${amountPart}${ref}`);
return `${part1}///${part2}`;
}
Express server example
Validate the request body, then call calculateSignature with the same sid and clientTimeStamp the client used for the request.
const express = require('express');
const cors = require('cors');
const { calculateSignature } = require('./signature-hint'); // your module implementing the functions above
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/generate-signature', (req, res) => {
const data = req.body;
const amountStr = typeof data.amount === 'string' ? data.amount : String(data.amount);
const amountNum = parseFloat(amountStr);
if (amountStr && amountNum <= 0) {
return res.status(400).json({ error: 'Invalid amount: amount must be greater than 0' });
}
const accountNumber = data.merchantAccountNumber || data.accountNumber;
if (!accountNumber) {
return res.status(400).json({ error: 'merchantAccountNumber or accountNumber is required' });
}
if (data.sid == null || data.sid === '') {
return res.status(400).json({ error: 'sid is required (client-generated session ID)' });
}
if (data.clientTimeStamp == null) {
return res.status(400).json({ error: 'clientTimeStamp is required (client-generated timestamp)' });
}
try {
const signature = calculateSignature(
data.clientTimeStamp,
data.sid,
amountStr,
data.orderId || '',
accountNumber,
data.merchantToken || null
);
res.json({ signature });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(process.env.PORT || 3001, () => {
console.log('Signature server running on http://localhost:' + (process.env.PORT || 3001));
});
Standalone HTTP server (no Express)
A zero-dependency Node.js server ships at server-side/node/server.js. It uses a shared library and exposes:
POST /api/generate-signature
GET / or GET /health
Request body:
{
"amount": "100.00",
"merchantAccountNumber": "ACCT-001",
"orderId": "ORD-12345",
"sid": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"clientTimeStamp": 1709912345678
}
Response:
{
"signature": "a1b2c3...///d4e5f6..."
}
Run:
cd server-side/node
npm install
MERCHANT_TOKEN=your-token node server.js
Python (signature steps)
Minimal illustration of the same hashing and concatenation. Validate inputs in your HTTP handler before calling calculate_signature.
import hashlib
import os
import uuid
from typing import Optional
MERCHANT_TOKEN = os.environ.get("MERCHANT_TOKEN", "your-merchant-token")
def hash256(message: str) -> str:
return hashlib.sha256(message.encode("utf-8")).hexdigest()
def _is_valid_uuid4(value: str) -> bool:
if not value or not isinstance(value, str):
return False
try:
u = uuid.UUID(value)
return u.version == 4
except (ValueError, AttributeError):
return False
def calculate_signature(
client_time_stamp,
sid: str,
amount: str,
reference_number: str,
account_number: str,
merchant_token: Optional[str] = None,
) -> str:
if not _is_valid_uuid4(sid):
raise ValueError("sid must be a valid GUID (UUID v4)")
amount_part = (
""
if amount is None or amount == "" or amount in ("undefined", "None")
else (amount if isinstance(amount, str) else str(amount))
)
token = merchant_token or MERCHANT_TOKEN
ref = reference_number or ""
part1 = hash256(f"{client_time_stamp}{sid}{amount_part}{ref}")
part2 = hash256(f"{client_time_stamp}{token}{account_number}{amount_part}{ref}")
return f"{part1}///{part2}"
Run the Python HTTP server
The repo includes a full HTTP server that uses the same library:
cd server-side/python
pip install -r requirements.txt
MERCHANT_TOKEN=your-token python server.py
POST /api/generate-signature uses the same JSON body and returns { "signature": "..." }.
Environment Variables
| Variable | Default | Description |
|---|---|---|
MERCHANT_TOKEN |
(use env in production) | Your secret merchant token |
PORT |
3001 (Node) / see Python server |
HTTP server port |
In production, always set
MERCHANT_TOKENvia environment variable or a secrets manager. Never commit your real token to source control.
Integration Flow
Client (Browser) Your Backend
───────────────── ────────────
1. sid = generateSessionId()
2. clientTimeStamp = Date.now()
3. POST /api/generate-signature ──────▶ 4. Validate inputs
{ amount, account, sid, ts } 5. Calculate signature
6. Return { signature }
7. signature = response.signature ◀─────
8. builder.setSignature(signature)
.setSid(sid)
.setClientTimeStamp(ts)
...
.send()