iCryptoNow

Code examples

Complete, dependency-free helpers for signing requests and verifying webhooks in five languages. Each file runs a self-test against our published test vectors.

Download

What each file contains

  • sign: builds X-Signature as described in Authentication.
  • request: calls the API with the three headers, signing exactly the bytes it sends.
  • verify webhook: checks the timestamp window and compares signatures in constant time, as described in Webhooks.
  • self-test: reproduces the signatures in vectors.json and rejects a tampered body and a stale timestamp.

Run the self-test

node gateway.mjs vectors.json

Expected output, for every language:

PASS request_post
PASS request_get
PASS webhook valid
PASS webhook tampered body rejected
PASS webhook stale timestamp rejected

Test vectors

Generated by the gateway's own signing code. Your implementation is correct when it produces the same signatures:

CaseExpected signature
request_posta05c4adc8dc0f7b7238f5053b169fb522f6bfa631c3389c5b12c4cab9d458f61
request_getd8d3bd60badac7f145fa0dd1573d26a42ef38ea502f2c614126c02a62e54a839
webhookfee4ee91af3e7e056105de9516497006be6be7703c6f21565d55a74a96089738
vectors.json
{
  "request_post": {
    "secret": "Zm9yLWRvY3Mtb25seS1ub3QtYS1yZWFsLXNlY3JldC0xMjM",
    "timestamp": "1767225600000",
    "method": "POST",
    "path": "/v1/payouts",
    "body": "{\"request_id\":\"wd-20260101-0001\",\"chain\":\"tron\",\"token\":\"USDT\",\"to\":\"TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf\",\"amount\":\"50000000\",\"player_ref\":\"player-42\"}",
    "signature": "a05c4adc8dc0f7b7238f5053b169fb522f6bfa631c3389c5b12c4cab9d458f61"
  },
  "request_get": {
    "secret": "Zm9yLWRvY3Mtb25seS1ub3QtYS1yZWFsLXNlY3JldC0xMjM",
    "timestamp": "1767225600000",
    "method": "GET",
    "path": "/v1/quote?chain=bsc&token=USDT&direction=in&amount=100000000000000000000",
    "body": "",
    "signature": "d8d3bd60badac7f145fa0dd1573d26a42ef38ea502f2c614126c02a62e54a839"
  },
  "webhook": {
    "secret": "whsec_ZXhhbXBsZS13ZWJob29rLXNlY3JldC1mb3ItZG9jcw",
    "timestamp": "1767225600123",
    "method": "POST",
    "path": "/callbacks/icryptonow",
    "body": "{\"event_id\":\"3f1c2a9e-8d4b-4c7a-9b1e-2f5d6c7a8b90\",\"type\":\"deposit.confirmed\",\"created_at\":\"2026-01-01T00:00:00.123Z\",\"data\":{\"deposit_id\":\"5b7c1d2e-3f4a-4b5c-8d6e-7f8a9b0c1d2e\",\"status\":\"confirmed\",\"chain\":\"tron\",\"asset\":\"tron:USDT\",\"decimals\":6,\"address\":\"TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf\",\"player_ref\":\"player-42\",\"amount\":\"100000000\",\"fee\":\"1000000\",\"net\":\"99000000\",\"finality\":\"finalized\"}}",
    "signature": "fee4ee91af3e7e056105de9516497006be6be7703c6f21565d55a74a96089738"
  }
}

Source

// iCryptoNow Gateway — request signing and webhook verification (Node.js 18+, no dependencies).
//
//   X-Signature = hex(HMAC-SHA256(secret, `${timestamp}\n${METHOD}\n${pathWithQuery}\n${rawBody}`))
//   timestamp   = unix milliseconds, within ±5 minutes of our clock
//
// Self-test against the published vectors:  node gateway.mjs ../vectors.json
import { createHmac, timingSafeEqual } from "node:crypto";
import { readFileSync } from "node:fs";

export function sign(secret, timestamp, method, pathWithQuery, body) {
  return createHmac("sha256", secret).update(`${timestamp}\n${method.toUpperCase()}\n${pathWithQuery}\n${body}`).digest("hex");
}

/** Call the gateway. `body` is an object (sent as JSON) or undefined. */
export async function request(baseUrl, keyId, secret, method, pathWithQuery, body) {
  const raw = body === undefined ? "" : JSON.stringify(body); // sign exactly the bytes you send
  const timestamp = String(Date.now());
  const res = await fetch(baseUrl + pathWithQuery, {
    method,
    headers: {
      "X-Key-Id": keyId,
      "X-Timestamp": timestamp,
      "X-Signature": sign(secret, timestamp, method, pathWithQuery, raw),
      ...(raw ? { "Content-Type": "application/json" } : {}),
    },
    ...(raw ? { body: raw } : {}),
  });
  return { status: res.status, body: await res.json() };
}

/**
 * Verify a webhook. `rawBody` must be the exact request body string (verify BEFORE parsing JSON);
 * `pathWithQuery` is the path of your callback URL as configured with us.
 */
export function verifyWebhook(secret, pathWithQuery, headers, rawBody, nowMs = Date.now()) {
  const timestamp = headers["x-timestamp"] ?? "";
  const signature = String(headers["x-signature"] ?? "");
  if (!/^\d{13}$/.test(timestamp) || Math.abs(nowMs - Number(timestamp)) > 5 * 60 * 1000) return false;
  if (!/^[0-9a-f]{64}$/.test(signature)) return false;
  const expected = sign(secret, timestamp, "POST", pathWithQuery, rawBody);
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}

// ── self-test ──
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").pop())) {
  const v = JSON.parse(readFileSync(process.argv[2] ?? "../vectors.json", "utf8"));
  const check = (name, ok) => {
    console.log(`${ok ? "PASS" : "FAIL"} ${name}`);
    if (!ok) process.exitCode = 1;
  };
  for (const k of ["request_post", "request_get"]) check(k, sign(v[k].secret, v[k].timestamp, v[k].method, v[k].path, v[k].body) === v[k].signature);
  const w = v.webhook;
  const h = { "x-timestamp": w.timestamp, "x-signature": w.signature };
  check("webhook valid", verifyWebhook(w.secret, w.path, h, w.body, Number(w.timestamp)));
  check("webhook tampered body rejected", !verifyWebhook(w.secret, w.path, h, w.body.replace("100000000", "900000000"), Number(w.timestamp)));
  check("webhook stale timestamp rejected", !verifyWebhook(w.secret, w.path, h, w.body, Number(w.timestamp) + 301_000));
}
Chat with us