Securing requests
Every call we make to your wallet is signed with a secret key only you and we hold. Check the signature before you read the body or move any money.
Headers
| Header | Value |
|---|---|
X-Client-ID | Who is calling. By default the game code, e.g. avionix: use it to pick the secret key. |
X-Client-TS | When the call was made: Unix time in seconds (UTC), e.g. 1793178000. |
X-Client-Signature | Lowercase hex HMAC-SHA256 of the string below, keyed with your secret key. |
How the signature is made
- Join, with nothing between them: the
X-Client-TSvalue, the request path (no host, no query string, e.g./vashub-wallet/withdraw), and the request body exactly as sent. - HMAC-SHA256 that string with your secret key, and hex-encode the result.
To verify a call, do the same on your side and compare the result with X-Client-Signature in constant time.
What to check
X-Client-IDis one you know. If not:401.X-Client-TSis within 5 minutes of your clock (keep your servers on NTP). If not:401.- The signature matches. If not:
401withmessage: "Invalid Client-Signature". - Only then parse the body and act on it.
Worked example
Use these values to test your code: it must produce the same signature.
| Input | Value |
|---|---|
| Secret key | test_secret_key |
X-Client-TS | 1793178000 |
| Path | /vashub-wallet/withdraw |
Body (as sent, one line)
{"user_id":"100245","session_token":"8f3c1d2e9b7a4c5d8e1f2a3b4c5d6e7f","currency":"KES","amount":100,"game":"avionix","action_id":5823019,"action":"bet","provider":"avionix","provider_tx_id":5823019,"platform":"mobile"}String to sign
1793178000/vashub-wallet/withdraw{"user_id":"100245","session_token":"8f3c1d2e9b7a4c5d8e1f2a3b4c5d6e7f","currency":"KES","amount":100,"game":"avionix","action_id":5823019,"action":"bet","provider":"avionix","provider_tx_id":5823019,"platform":"mobile"}X-Client-Signature
3f54d0332e49d022e51d3635da0dc42593b7d4042c1443fa0688727afb1ac250Pretty-printed, the same body reads:
{
"user_id": "100245",
"session_token": "8f3c1d2e9b7a4c5d8e1f2a3b4c5d6e7f",
"currency": "KES",
"amount": 100,
"game": "avionix",
"action_id": 5823019,
"action": "bet",
"provider": "avionix",
"provider_tx_id": 5823019,
"platform": "mobile"
}Verifying in your code
import crypto from "node:crypto";
// X-Client-ID -> secret key (from your onboarding email; keep it in your secrets store).
const CLIENTS = { avionix: process.env.VASHUB_SECRET_AVIONIX };
// rawBody: the request body exactly as received (a Buffer), before any JSON parsing.
// path: the request path without the query string, e.g. "/vashub-wallet/withdraw".
export function isSignedByVasHub(headers, path, rawBody) {
const secret = CLIENTS[headers["x-client-id"]];
const ts = headers["x-client-ts"] ?? "";
const given = Buffer.from(headers["x-client-signature"] ?? "", "hex");
if (!secret || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(ts).update(path).update(rawBody).digest();
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}Sending a test call
Call your own wallet the way our game server does, to check your verification and replies end to end.
// Sends a signed test call to your wallet, the way our game server does.
import crypto from "node:crypto";
const BASE = "https://wallet.yourcasino.com/vashub-wallet"; // your wallet base URL
const SECRET = process.env.VASHUB_SECRET_AVIONIX;
async function call(method, body) {
const url = new URL(`${BASE}/${method}`);
const payload = JSON.stringify(body);
const ts = Math.floor(Date.now() / 1000).toString();
const signature = crypto.createHmac("sha256", SECRET).update(ts + url.pathname + payload).digest("hex");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client-ID": "avionix", "X-Client-TS": ts, "X-Client-Signature": signature },
body: payload,
});
return { status: res.status, body: await res.json() };
}
console.log(await call("withdraw", {
user_id: "100245", session_token: "<a launch token>", currency: "KES", amount: 10, game: "avionix",
action_id: 900000001, action: "bet", provider: "avionix", provider_tx_id: 900000001, platform: "mobile",
}));More protection
- HTTPS only. Your wallet base URL must use TLS with a valid certificate.
- Allow-list our servers. We send the IP addresses our game servers call you from; you can refuse anything else with
403. - Keep the secret secret. Store it in your secrets manager, never in front-end code or a repository. Ask us to rotate it any time.