JavaScript (Node.js) SDK Examples
This page provides a complete Node.js integration example for the Ruby Team API, including the HMAC-SHA256 signing utility, an HTTP client wrapper using axios, and Express middleware for Seamless Wallet callback verification.
Dependencies: axios (install with npm install axios)
Signing Utility and HTTP Client
const crypto = require("crypto");
const axios = require("axios");
class RubyApiClient {
/**
* HTTP client for the Ruby Team API with automatic HMAC-SHA256 signing.
*
* @param {string} baseUrl - e.g. "https://api-test.ruby-gaming.com"
* @param {string} apiKey - Your team_api_key
* @param {string} apiSecret - Your team_api_secret
*/
constructor(baseUrl, apiKey, apiSecret) {
this.baseUrl = baseUrl.replace(/\/$/, "");
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}
/**
* Compute the HMAC-SHA256 signature for a Team API request.
*
* Signature string: {timestamp}{METHOD}{path}{rawBody}
* - timestamp: Unix seconds as a decimal string
* - METHOD: Uppercase HTTP method (GET, POST, PUT, ...)
* - path: Request path including query string if present
* - rawBody: Raw request body string; empty string for no-body requests
*
* @param {string} method
* @param {string} path - Path including query string, e.g. "/api/bet/list?page=1&size=20"
* @param {string} body - Raw body string (default "")
* @returns {{ timestamp: number, signature: string }}
*/
_sign(method, path, body = "") {
const timestamp = Math.floor(Date.now() / 1000);
const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
const signature = crypto
.createHmac("sha256", this.apiSecret)
.update(message, "utf8")
.digest("hex");
return { timestamp, signature };
}
/**
* Build the three required authentication headers plus Content-Type.
*
* @param {string} method
* @param {string} path
* @param {string} body
* @returns {object}
*/
_buildHeaders(method, path, body = "") {
const { timestamp, signature } = this._sign(method, path, body);
return {
"X-Team-Key": this.apiKey,
"X-Team-Timestamp": String(timestamp),
"X-Team-Signature": signature,
"Content-Type": "application/json",
};
}
/**
* Send an authenticated GET request.
*
* @param {string} path - e.g. "/api/bet/list"
* @param {object} params - Query parameters as a plain object
* @returns {Promise<object>} Parsed JSON response body
*/
async get(path, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullPath = queryString ? `${path}?${queryString}` : path;
const headers = this._buildHeaders("GET", fullPath, "");
const response = await axios.get(this.baseUrl + fullPath, { headers });
return response.data;
}
/**
* Send an authenticated POST request with a JSON body.
*
* @param {string} path
* @param {object} body
* @returns {Promise<object>} Parsed JSON response body
*/
async post(path, body) {
// Serialize once — the same string is used for signing and as the request body
const rawBody = JSON.stringify(body);
const headers = this._buildHeaders("POST", path, rawBody);
const response = await axios.post(this.baseUrl + path, rawBody, { headers });
return response.data;
}
/**
* Send an authenticated PUT request with a JSON body.
*
* @param {string} path
* @param {object} body
* @returns {Promise<object>} Parsed JSON response body
*/
async put(path, body) {
const rawBody = JSON.stringify(body);
const headers = this._buildHeaders("PUT", path, rawBody);
const response = await axios.put(this.baseUrl + path, rawBody, { headers });
return response.data;
}
}
module.exports = { RubyApiClient };
The body used for signing must be byte-for-byte identical to the body sent in the HTTP request. The examples above pass the same rawBody string to both the signing function and to axios. Do not reformat or re-serialize the body between signing and sending.
Example: Create a Brand
const { RubyApiClient } = require("./ruby-client");
const client = new RubyApiClient(
"https://api-test.ruby-gaming.com",
"your_team_api_key",
"your_team_api_secret"
);
async function createBrand() {
const data = await client.post("/api/brand/create", {
name: "My Brand",
code: "mybrand01",
wallet_mode: "seamless",
callback_url: "https://mybrand.example.com/ruby/callback",
currency: "KRW",
});
console.log(data);
// {
// id: 42,
// name: "My Brand",
// code: "mybrand01",
// api_key: "aBcDeFgH...",
// api_secret: "xYzSeCrEt...", <-- save this, returned only once
// wallet_mode: "seamless",
// status: 1,
// ...
// }
const { api_key, api_secret } = data;
// Store api_secret in your secret manager — it is never returned again
return data;
}
createBrand().catch(console.error);
api_secret immediatelyThe api_secret field is returned only in the create response. Store it in your secret manager before proceeding.
Example: Query Bet List
async function listBets(brandId) {
const data = await client.get("/api/bet/list", {
page: 1,
size: 20,
brand_id: brandId,
});
console.log(`Total bets: ${data.total}`);
data.items.forEach((bet) => console.log(bet));
return data;
}
listBets(42).catch(console.error);
Callback Signature Verification
If your brand uses wallet_mode: "seamless", Ruby will call your server for balance, debit, credit, and rollback operations. Each request is signed with an HMAC-SHA256 signature. You must verify it before processing the request.
Callback verification uses a different signing algorithm from the Team API. The HMAC input is Buffer.concat([bodyBytes, Buffer.from(timestamp, "utf8")]) — body first, timestamp at the end. The key is your brand.api_secret, not your team_api_secret.
const crypto = require("crypto");
/**
* Verify the HMAC-SHA256 signature on an incoming Ruby callback request.
*
* @param {string} apiSecret - Your brand's api_secret
* @param {Buffer} bodyBytes - Raw request body as a Buffer (do NOT parse first)
* @param {string} timestamp - Value of the X-Aggregator-Timestamp header
* @param {string} signature - Value of the X-Aggregator-Signature header
* @param {number} maxAgeSeconds - Reject requests older than this (default 300)
* @returns {boolean}
*/
function verifyCallbackSignature(
apiSecret,
bodyBytes,
timestamp,
signature,
maxAgeSeconds = 300
) {
// Validate timestamp freshness
const ts = parseInt(timestamp, 10);
if (isNaN(ts)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > maxAgeSeconds) return false;
// Callback signing: HMAC-SHA256(key=apiSecret, data=body_bytes + timestamp_bytes)
const hmacInput = Buffer.concat([bodyBytes, Buffer.from(timestamp, "utf8")]);
const expected = crypto
.createHmac("sha256", apiSecret)
.update(hmacInput)
.digest("hex");
// Constant-time comparison to prevent timing attacks
if (expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected, "utf8"),
Buffer.from(signature, "utf8")
);
}
module.exports = { verifyCallbackSignature };
Express Middleware Example
const express = require("express");
const { verifyCallbackSignature } = require("./verify-callback");
const app = express();
const BRAND_API_KEY = "key_brandabc"; // From create brand response
const BRAND_API_SECRET = "my_brand_secret"; // From create brand response
// IMPORTANT: Use raw body middleware so we capture bytes before JSON parsing
app.use(
express.json({
verify: (req, _res, buf) => {
req.rawBody = buf; // Store raw Buffer on the request object
},
})
);
/**
* Shared signature verification middleware for all callback routes.
*/
function verifySignature(req, res, next) {
const valid = verifyCallbackSignature(
BRAND_API_SECRET,
req.rawBody,
req.headers["x-aggregator-timestamp"] ?? "",
req.headers["x-aggregator-signature"] ?? ""
);
if (!valid) {
return res.status(401).json({ error: "Invalid signature" });
}
// Verify the API key matches your brand
if (req.headers["x-aggregator-key"] !== BRAND_API_KEY) {
return res.status(401).json({ error: "Unknown API key" });
}
next();
}
const router = express.Router();
/** Balance inquiry — return current player balance. */
router.post("/callback/balance", verifySignature, (req, res) => {
return res.json({ balance: "1250.00" });
});
/** Debit — deduct from player wallet (must be idempotent on transaction_id). */
router.post("/callback/debit", verifySignature, (req, res) => {
return res.json({ balance: "1149.50", balance_before: "1250.00" });
});
/** Credit — add winnings to player wallet (must be idempotent on transaction_id). */
router.post("/callback/credit", verifySignature, (req, res) => {
return res.json({ balance: "1350.00", balance_before: "1250.00" });
});
/** Rollback — reverse a previous debit (must be idempotent on transaction_id). */
router.post("/callback/rollback", verifySignature, (req, res) => {
return res.json({ balance: "1250.00" });
});
app.use(router);
app.listen(3000);
Common Mistakes
| Mistake | Result | Fix |
|---|---|---|
| Query string missing from path during signing | Signature mismatch (401) | Always include ?key=value in the path when signing GET requests |
| Re-serializing JSON body after signing | Signature mismatch (401) | Pass the same string to both the signer and the HTTP client |
| Stale timestamp (>300 s drift) | Request rejected (401) | Sync system clock with NTP |
Using team_api_secret for callback verification | Verification fails | Callback uses brand.api_secret, not team_api_secret |
Using express.json() without a verify hook | Cannot read raw bytes | Add the verify callback to capture req.rawBody before parsing |
String equality instead of timingSafeEqual | Timing attack vulnerability | Always use crypto.timingSafeEqual for signature comparison |