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-Signatureas 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.jsonand rejects a tampered body and a stale timestamp.
Run the self-test
node gateway.mjs vectors.json
python3 gateway.py vectors.json
php gateway.php vectors.json
go run gateway.go vectors.json
java Gateway.java 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:
| Case | Expected signature |
|---|---|
request_post | a05c4adc8dc0f7b7238f5053b169fb522f6bfa631c3389c5b12c4cab9d458f61 |
request_get | d8d3bd60badac7f145fa0dd1573d26a42ef38ea502f2c614126c02a62e54a839 |
webhook | fee4ee91af3e7e056105de9516497006be6be7703c6f21565d55a74a96089738 |
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)); }
"""iCryptoNow Gateway — request signing and webhook verification (Python 3.8+, standard library only). X-Signature = hex(HMAC-SHA256(secret, f"{timestamp}\\n{METHOD}\\n{path_with_query}\\n{raw_body}")) timestamp = unix milliseconds, within ±5 minutes of our clock Self-test against the published vectors: python3 gateway.py ../vectors.json """ import hashlib import hmac import json import re import sys import time import urllib.request def sign(secret: str, timestamp: str, method: str, path_with_query: str, body: str) -> str: message = f"{timestamp}\n{method.upper()}\n{path_with_query}\n{body}" return hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() def request(base_url: str, key_id: str, secret: str, method: str, path_with_query: str, body=None): """Call the gateway. `body` is a dict (sent as JSON) or None.""" raw = "" if body is None else json.dumps(body, separators=(",", ":")) # sign exactly the bytes you send timestamp = str(int(time.time() * 1000)) headers = {"X-Key-Id": key_id, "X-Timestamp": timestamp, "X-Signature": sign(secret, timestamp, method, path_with_query, raw)} if raw: headers["Content-Type"] = "application/json" req = urllib.request.Request(base_url + path_with_query, data=raw.encode() if raw else None, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=15) as res: return res.status, json.load(res) except urllib.error.HTTPError as err: return err.code, json.load(err) def verify_webhook(secret: str, path_with_query: str, headers: dict, raw_body: str, now_ms=None) -> bool: """`raw_body` must be the exact request body (verify BEFORE parsing JSON); `path_with_query` is your callback path.""" now_ms = int(time.time() * 1000) if now_ms is None else now_ms timestamp = headers.get("x-timestamp", "") signature = headers.get("x-signature", "") if not re.fullmatch(r"\d{13}", timestamp) or abs(now_ms - int(timestamp)) > 5 * 60 * 1000: return False if not re.fullmatch(r"[0-9a-f]{64}", signature): return False return hmac.compare_digest(sign(secret, timestamp, "POST", path_with_query, raw_body), signature) if __name__ == "__main__": v = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "../vectors.json")) failed = False def check(name, ok): global failed print(("PASS " if ok else "FAIL ") + name) failed |= not ok for k in ("request_post", "request_get"): x = v[k] check(k, sign(x["secret"], x["timestamp"], x["method"], x["path"], x["body"]) == x["signature"]) w = v["webhook"] h = {"x-timestamp": w["timestamp"], "x-signature": w["signature"]} check("webhook valid", verify_webhook(w["secret"], w["path"], h, w["body"], int(w["timestamp"]))) check("webhook tampered body rejected", not verify_webhook(w["secret"], w["path"], h, w["body"].replace("100000000", "900000000"), int(w["timestamp"]))) check("webhook stale timestamp rejected", not verify_webhook(w["secret"], w["path"], h, w["body"], int(w["timestamp"]) + 301_000)) sys.exit(1 if failed else 0)
<?php // iCryptoNow Gateway — request signing and webhook verification (PHP 7.4+, ext-curl). // // 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: php gateway.php ../vectors.json function gw_sign(string $secret, string $timestamp, string $method, string $pathWithQuery, string $body): string { return hash_hmac('sha256', $timestamp . "\n" . strtoupper($method) . "\n" . $pathWithQuery . "\n" . $body, $secret); } /** Call the gateway. $body is an array (sent as JSON) or null. Returns [httpStatus, decodedBody]. */ function gw_request(string $baseUrl, string $keyId, string $secret, string $method, string $pathWithQuery, ?array $body = null): array { $raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // sign exactly what you send $timestamp = (string) intdiv((int) (microtime(true) * 1000000), 1000); $headers = [ 'X-Key-Id: ' . $keyId, 'X-Timestamp: ' . $timestamp, 'X-Signature: ' . gw_sign($secret, $timestamp, $method, $pathWithQuery, $raw), ]; if ($raw !== '') { $headers[] = 'Content-Type: application/json'; } $ch = curl_init($baseUrl . $pathWithQuery); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]); if ($raw !== '') { curl_setopt($ch, CURLOPT_POSTFIELDS, $raw); } $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return [$status, json_decode((string) $response, true)]; } /** * Verify a webhook. $rawBody = file_get_contents('php://input') — verify BEFORE json_decode. * $pathWithQuery is the path of your callback URL as configured with us. */ function gw_verify_webhook(string $secret, string $pathWithQuery, string $timestamp, string $signature, string $rawBody, ?int $nowMs = null): bool { $nowMs = $nowMs ?? intdiv((int) (microtime(true) * 1000000), 1000); if (!preg_match('/^\d{13}$/', $timestamp) || abs($nowMs - (int) $timestamp) > 5 * 60 * 1000) { return false; } if (!preg_match('/^[0-9a-f]{64}$/', $signature)) { return false; } return hash_equals(gw_sign($secret, $timestamp, 'POST', $pathWithQuery, $rawBody), $signature); } // In your callback endpoint: // $raw = file_get_contents('php://input'); // if (!gw_verify_webhook($secret, '/callbacks/icryptonow', $_SERVER['HTTP_X_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_SIGNATURE'] ?? '', $raw)) { // http_response_code(401); exit; // } // $event = json_decode($raw, true); // dedupe on $event['event_id'], then respond 200 quickly // ── self-test ── if (PHP_SAPI === 'cli' && realpath($argv[0]) === __FILE__) { $v = json_decode(file_get_contents($argv[1] ?? '../vectors.json'), true); $failed = false; $check = function (string $name, bool $ok) use (&$failed) { echo ($ok ? 'PASS ' : 'FAIL ') . $name . PHP_EOL; $failed = $failed || !$ok; }; foreach (['request_post', 'request_get'] as $k) { $x = $v[$k]; $check($k, gw_sign($x['secret'], $x['timestamp'], $x['method'], $x['path'], $x['body']) === $x['signature']); } $w = $v['webhook']; $check('webhook valid', gw_verify_webhook($w['secret'], $w['path'], $w['timestamp'], $w['signature'], $w['body'], (int) $w['timestamp'])); $check('webhook tampered body rejected', !gw_verify_webhook($w['secret'], $w['path'], $w['timestamp'], $w['signature'], str_replace('100000000', '900000000', $w['body']), (int) $w['timestamp'])); $check('webhook stale timestamp rejected', !gw_verify_webhook($w['secret'], $w['path'], $w['timestamp'], $w['signature'], $w['body'], (int) $w['timestamp'] + 301000)); exit($failed ? 1 : 0); }
// iCryptoNow Gateway — request signing and webhook verification (Go 1.20+, standard library only). // // 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: go run gateway.go ../vectors.json package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "regexp" "strconv" "strings" "time" ) func Sign(secret, timestamp, method, pathWithQuery, body string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + "\n" + strings.ToUpper(method) + "\n" + pathWithQuery + "\n" + body)) return hex.EncodeToString(mac.Sum(nil)) } // Request calls the gateway. body is any JSON-serialisable value, or nil. func Request(baseURL, keyID, secret, method, pathWithQuery string, body any) (int, []byte, error) { raw := []byte{} if body != nil { var err error if raw, err = json.Marshal(body); err != nil { // sign exactly the bytes you send return 0, nil, err } } timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) req, err := http.NewRequest(method, baseURL+pathWithQuery, bytes.NewReader(raw)) if err != nil { return 0, nil, err } req.Header.Set("X-Key-Id", keyID) req.Header.Set("X-Timestamp", timestamp) req.Header.Set("X-Signature", Sign(secret, timestamp, method, pathWithQuery, string(raw))) if len(raw) > 0 { req.Header.Set("Content-Type", "application/json") } res, err := (&http.Client{Timeout: 15 * time.Second}).Do(req) if err != nil { return 0, nil, err } defer res.Body.Close() out, err := io.ReadAll(res.Body) return res.StatusCode, out, err } var tsRe = regexp.MustCompile(`^\d{13}$`) var sigRe = regexp.MustCompile(`^[0-9a-f]{64}$`) // VerifyWebhook checks a callback. rawBody is the exact request body (verify BEFORE unmarshalling); // pathWithQuery is the path of your callback URL as configured with us. func VerifyWebhook(secret, pathWithQuery, timestamp, signature, rawBody string, nowMs int64) bool { if !tsRe.MatchString(timestamp) || !sigRe.MatchString(signature) { return false } ts, _ := strconv.ParseInt(timestamp, 10, 64) if d := nowMs - ts; d > 5*60*1000 || d < -5*60*1000 { return false } return hmac.Equal([]byte(Sign(secret, timestamp, "POST", pathWithQuery, rawBody)), []byte(signature)) } // ── self-test ── type vector struct{ Secret, Timestamp, Method, Path, Body, Signature string } func main() { path := "../vectors.json" if len(os.Args) > 1 { path = os.Args[1] } data, err := os.ReadFile(path) if err != nil { panic(err) } var v map[string]vector if err := json.Unmarshal(data, &v); err != nil { panic(err) } failed := false check := func(name string, ok bool) { if ok { fmt.Println("PASS", name) } else { fmt.Println("FAIL", name) failed = true } } for _, k := range []string{"request_post", "request_get"} { x := v[k] check(k, Sign(x.Secret, x.Timestamp, x.Method, x.Path, x.Body) == x.Signature) } w := v["webhook"] ts, _ := strconv.ParseInt(w.Timestamp, 10, 64) check("webhook valid", VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, w.Body, ts)) check("webhook tampered body rejected", !VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, strings.Replace(w.Body, "100000000", "900000000", 1), ts)) check("webhook stale timestamp rejected", !VerifyWebhook(w.Secret, w.Path, w.Timestamp, w.Signature, w.Body, ts+301000)) if failed { os.Exit(1) } }
// iCryptoNow Gateway — request signing and webhook verification (Java 11+, JDK only). // // 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: java Gateway.java ../vectors.json import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.time.Duration; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class Gateway { public static String sign(String secret, String timestamp, String method, String pathWithQuery, String body) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); String message = timestamp + "\n" + method.toUpperCase(Locale.ROOT) + "\n" + pathWithQuery + "\n" + body; StringBuilder hex = new StringBuilder(); for (byte b : mac.doFinal(message.getBytes(StandardCharsets.UTF_8))) hex.append(String.format("%02x", b)); return hex.toString(); } /** Call the gateway. jsonBody is the JSON string you send (sign exactly these bytes), or "" for none. */ public static HttpResponse<String> request(String baseUrl, String keyId, String secret, String method, String pathWithQuery, String jsonBody) throws Exception { String timestamp = String.valueOf(System.currentTimeMillis()); HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(baseUrl + pathWithQuery)) .timeout(Duration.ofSeconds(15)) .header("X-Key-Id", keyId) .header("X-Timestamp", timestamp) .header("X-Signature", sign(secret, timestamp, method, pathWithQuery, jsonBody)); if (jsonBody.isEmpty()) { b.method(method, HttpRequest.BodyPublishers.noBody()); } else { b.header("Content-Type", "application/json").method(method, HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8)); } return HttpClient.newHttpClient().send(b.build(), HttpResponse.BodyHandlers.ofString()); } /** Verify a callback: rawBody is the exact request body (verify BEFORE parsing); pathWithQuery is your callback path. */ public static boolean verifyWebhook(String secret, String pathWithQuery, String timestamp, String signature, String rawBody, long nowMs) throws Exception { if (timestamp == null || !timestamp.matches("\\d{13}") || signature == null || !signature.matches("[0-9a-f]{64}")) return false; if (Math.abs(nowMs - Long.parseLong(timestamp)) > 5 * 60 * 1000) return false; byte[] expected = sign(secret, timestamp, "POST", pathWithQuery, rawBody).getBytes(StandardCharsets.US_ASCII); return MessageDigest.isEqual(expected, signature.getBytes(StandardCharsets.US_ASCII)); } // ── self-test (tiny JSON reader for the flat vectors file; use your JSON library in real code) ── static String field(String json, String section, String key) { int s = json.indexOf("\"" + section + "\""); int k = json.indexOf("\"" + key + "\"", s); int i = json.indexOf('"', json.indexOf(':', k) + 1) + 1; StringBuilder out = new StringBuilder(); for (; json.charAt(i) != '"'; i++) { char c = json.charAt(i); if (c == '\\') { char n = json.charAt(++i); out.append(n == 'n' ? '\n' : n); } else out.append(c); } return out.toString(); } static boolean failed = false; static void check(String name, boolean ok) { System.out.println((ok ? "PASS " : "FAIL ") + name); failed |= !ok; } public static void main(String[] args) throws Exception { String json = Files.readString(Path.of(args.length > 0 ? args[0] : "../vectors.json")); for (String k : new String[] {"request_post", "request_get"}) { check(k, sign(field(json, k, "secret"), field(json, k, "timestamp"), field(json, k, "method"), field(json, k, "path"), field(json, k, "body")) .equals(field(json, k, "signature"))); } String sec = field(json, "webhook", "secret"), ts = field(json, "webhook", "timestamp"), p = field(json, "webhook", "path"); String sig = field(json, "webhook", "signature"), body = field(json, "webhook", "body"); long now = Long.parseLong(ts); check("webhook valid", verifyWebhook(sec, p, ts, sig, body, now)); check("webhook tampered body rejected", !verifyWebhook(sec, p, ts, sig, body.replace("100000000", "900000000"), now)); check("webhook stale timestamp rejected", !verifyWebhook(sec, p, ts, sig, body, now + 301000)); if (failed) System.exit(1); } }