Skip to content
VAS HUB
Info

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

HeaderValue
X-Client-IDWho is calling. By default the game code, e.g. avionix: use it to pick the secret key.
X-Client-TSWhen the call was made: Unix time in seconds (UTC), e.g. 1793178000.
X-Client-SignatureLowercase hex HMAC-SHA256 of the string below, keyed with your secret key.

How the signature is made

  1. Join, with nothing between them: the X-Client-TS value, the request path (no host, no query string, e.g. /vashub-wallet/withdraw), and the request body exactly as sent.
  2. 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

  1. X-Client-ID is one you know. If not: 401.
  2. X-Client-TS is within 5 minutes of your clock (keep your servers on NTP). If not: 401.
  3. The signature matches. If not: 401 with message: "Invalid Client-Signature".
  4. Only then parse the body and act on it.

Worked example

Use these values to test your code: it must produce the same signature.

InputValue
Secret keytest_secret_key
X-Client-TS1793178000
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
3f54d0332e49d022e51d3635da0dc42593b7d4042c1443fa0688727afb1ac250

Pretty-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.