1. Overview
Every signed API request includes a sign field — a cryptographic hash that proves the request has not been tampered with and comes from a known partner.
As a new customer, both signature enhancements are enabled for your integration from day one:
- Pipe delimiter between every field in the signing string
- Nonce + timestamp appended as the last two fields for replay protection
This document describes the combined format you will use for all signed requests.
2. Building the signing string
Concatenate the values of the parameters documented for that API method, in the order specified in that method's article, separated by a pipe | character. Then append timestamp and nonce as the final two values:
signData = value1|value2|...|valueN|timestamp|nonce
Rules:
- Use the field order exactly as listed in each method's article — order matters
- Include every field, even if its value is an empty string — the pipe is always present
partnerIDis always the first field, formatted as an uppercase GUID with hyphens (e.g.59F0ECB9-3316-4ECB-AE5F-57D34DD53C31)- Any
amountfield must be normalized to exactly 2 decimal places before signing —12becomes12.00,12.3becomes12.30 timestampandnonceare always the last two fields
3. Timestamp and nonce
| Field | Type | Format | Example |
|---|---|---|---|
timestamp | string | Unix epoch seconds, UTC, decimal integer | "1731398400" |
nonce | string | UUID v4, hyphenated, case-insensitive | "f47ac10b-58cc-4372-a567-0e02b2c3d479" |
- Generate a fresh
timestampon every request — do not cache or reuse - Generate a fresh
nonceon every request — reuse within 300 seconds is rejected as a replay - Both must also be sent as explicit fields in the request body alongside
sign
Timestamp freshness window
Both THG and your system apply a 5-minute (300-second) acceptance window for timestamps.
THG's side: any request you send with a timestamp older than 5 minutes from THG's current UTC clock will be rejected. Ensure your server clock is synchronised via NTP.
Your side: you should apply the same rule when verifying inbound THG callbacks — reject any callback whose timestamp is more than 5 minutes old from your current UTC clock. This protects your system against replay attacks on the callback channel.
now_utc - timestamp <= 300 // accept (timestamp is fresh) now_utc - timestamp > 300 // reject (timestamp too old)
4. Signing algorithm
sign = HMAC-SHA256(secretKey, signData)
- Key: your per-partner shared secret, UTF-8 encoded
- Message: the
signDatastring from §2, UTF-8 encoded - Output: 64 hexadecimal characters, no separators (case-insensitive — uppercase, lowercase, or mixed are all accepted)
5. Examples
Example 1 — GetGameModules
Method sign fields: partnerID, channel
partnerID = "59F0ECB9-3316-4ECB-AE5F-57D34DD53C31" channel = "" (empty) timestamp = "1786814779" nonce = "ac3e5f12-2faa-4c2e-8a9f-0a1af6d97619" signData = "59F0ECB9-3316-4ECB-AE5F-57D34DD53C31||1786814779|ac3e5f12-2faa-4c2e-8a9f-0a1af6d97619"
Note: channel is empty but the pipe is still present — giving || between partnerID and timestamp.
Example 2 — GetGameLaunchToken
Method sign fields: partnerID, name, displayName, currency, countrycode, type, parent, password, details
partnerID = "59F0ECB9-3316-4ECB-AE5F-57D34DD53C31" name = "Yura_Bobrik" displayName = "" currency = "EUR" countrycode = "" type = "0" parent = "" password = "" details = "" timestamp = "1786957743" nonce = "9702334e-2885-4108-9da4-645709cb4650" signData = "59F0ECB9-3316-4ECB-AE5F-57D34DD53C31|Yura_Bobrik||EUR||0|||1786957743|9702334e-2885-4108-9da4-645709cb4650"
Every empty field still has a pipe on each side.
6. Server-side validation
Every signed request is validated in this order — rejected at the first failing check:
| # | Check | Error |
|---|---|---|
| 1 | timestamp and nonce both present and non-empty | ArgumentError |
| 2 | timestamp not older than 300 seconds from server UTC clock | ArgumentFormatError |
| 3 | nonce is a valid UUID v4 | ArgumentFormatError |
| 4 | nonce not seen before within the 300s window | ErrorInvalidSign |
| 5 | HMAC matches recomputed signature | ErrorInvalidSign |
7. Code samples
PHP
<?php
function buildSign(string $secret, array $parts, string $timestamp, string $nonce): array
{
$signData = implode('|', array_map('strval', $parts)) . '|' . $timestamp . '|' . $nonce;
return [
'sign' => hash_hmac('sha256', $signData, $secret), // 64 hex characters, case-insensitive
'timestamp' => $timestamp,
'nonce' => $nonce,
];
}
// Verify inbound THG callback timestamp (5-minute window)
function isTimestampFresh(string $timestamp): bool {
return (time() - (int)$timestamp) <= 300;
}
$timestamp = (string) time();
$nonce = strtolower(sprintf('%s-%s-4%s-%x%s-%s',
bin2hex(random_bytes(4)),
bin2hex(random_bytes(2)),
substr(bin2hex(random_bytes(2)), 1),
0x8 | (ord(random_bytes(1)) & 0x3),
substr(bin2hex(random_bytes(2)), 1),
bin2hex(random_bytes(6))
));
Node.js / JavaScript
const crypto = require('crypto');
function buildSign(secret, parts, timestamp, nonce) {
const signData = parts.map(String).join('|') + '|' + timestamp + '|' + nonce;
return {
sign: crypto.createHmac('sha256', secret).update(signData, 'utf8').digest('hex'),
timestamp,
nonce,
};
}
// Verify inbound THG callback timestamp (5-minute window)
function isTimestampFresh(timestamp) {
return (Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) <= 300;
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID(); // Node 14.17+
Python 3
import hmac, hashlib, time, uuid
def build_sign(secret: str, parts: list, timestamp: str, nonce: str) -> dict:
sign_data = '|'.join(str(p) for p in parts) + '|' + timestamp + '|' + nonce
sign = hmac.new(secret.encode('utf-8'), sign_data.encode('utf-8'), hashlib.sha256).hexdigest()
return {'sign': sign, 'timestamp': timestamp, 'nonce': nonce}
def is_timestamp_fresh(timestamp: str) -> bool:
"""Verify inbound THG callback timestamp — 5-minute window."""
return (int(time.time()) - int(timestamp)) <= 300
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
C#
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
public static class Signature
{
public static (string sign, string timestamp, string nonce) Build(string secretKey, string[] parts)
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
string nonce = Guid.NewGuid().ToString(); // case-insensitive UUID v4
string signData = string.Join("|", parts) + "|" + timestamp + "|" + nonce;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
string sign = BitConverter.ToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(signData)))
.Replace("-", "").ToLowerInvariant();
return (sign, timestamp, nonce);
}
// Verify inbound THG callback timestamp — 5-minute window
public static bool IsTimestampFresh(string timestamp)
{
if (!long.TryParse(timestamp, out long ts)) return false;
return (DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) <= 300;
}
}
Java
import java.time.Instant;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class Signature {
public record Result(String sign, String timestamp, String nonce) {}
public static Result build(String secretKey, String[] parts) throws Exception {
String ts = Long.toString(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String data = String.join("|", parts) + "|" + ts + "|" + nonce;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"));
byte[] raw = mac.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : raw) sb.append(String.format("%02x", b));
return new Result(sb.toString(), ts, nonce);
}
// Verify inbound THG callback timestamp — 5-minute window
public static boolean isTimestampFresh(String timestamp) {
try {
long ts = Long.parseLong(timestamp);
return (Instant.now().getEpochSecond() - ts) <= 300;
} catch (NumberFormatException e) {
return false;
}
}
}
8. Applies to
Integration Service, Wallet Service, and Report Service. Every signed request you send to THG and every Seamless Wallet callback THG sends to you uses this signing format.
Comments
0 comments
Article is closed for comments.