Skip to main content

Authentication

Every request Alogame sends to your server is signed. Verify the signature on every incoming request before doing any work, and reject anything that fails or is too old.

Which algorithm applies depends on your contract:

Your contractAlgorithmHeaders
Web Payment — StandardHMAC-SHA256x-signature, x-timestamp
Mobile IAP and Web Payment — ExpubMD5Signature
In-Game Top-Up — signed deep linkHMAC-SHA256, sorted query stringquery param deeplink_sig

Your secret_key is issued by Alogame. Never expose it in a client build, a public repository, or a log line.


HMAC-SHA256 — standard contract

Every request carries two headers:

HeaderValue
x-timestampUnix timestamp in milliseconds
x-signatureHMAC-SHA256(secret_key, x-timestamp + raw_request_body), lowercase hex

The signed string is the x-timestamp value concatenated directly with the raw JSON request body — no separator, no sorting, no URL-encoding.

Sign the raw body, never a re-serialized object

The signature covers the exact bytes Alogame sent. If you parse the JSON and re-serialize it before hashing, key order, spacing, and number formatting can all change, and every signature will fail.

Capture the raw body before your framework's JSON parser consumes it:

  • Express: express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })
  • Laravel / PHP: file_get_contents('php://input')
  • Spring: ContentCachingRequestWrapper

Reject the request if:

  • Computed signature ≠ x-signature
  • |now − x-timestamp| > 600000 ms (10-minute replay window — note the header is in milliseconds, unlike the expub contract which uses seconds)

Compare with a constant-time function (hash_equals, hmac.compare_digest, crypto.timingSafeEqual), not ==.

PHP
function verifySignature(string $rawBody, string $timestamp, string $secret, string $received): bool {
$expected = hash_hmac('sha256', $timestamp . $rawBody, $secret);
return hash_equals($expected, $received);
}

$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
Python
import hmac, hashlib

def verify_signature(raw_body: bytes, timestamp: str, secret: str, received: str) -> bool:
expected = hmac.new(
secret.encode(),
timestamp.encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received)
Node.js
const crypto = require("crypto");

function verifySignature(rawBody, timestamp, secret, received) {
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + rawBody)
.digest("hex");

const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

MD5 — mobile IAP and expub contract

Every request from Alogame includes a Signature header. Verify it on every incoming request before processing.

Signature algorithm

Signature = MD5(sorted_query_string(body) + secret_key + timestamp)

Steps:

  1. Convert the JSON body to query-string format: key=value&key2=value2
  2. Sort keys alphabetically
  3. Append your secret_key (provided by Alogame — never expose this)
  4. Append the timestamp value from the request body (Unix seconds)
  5. MD5-hash the resulting string

Example — body { "userId": "abc-123", "timestamp": 1678886400 }:

# Sorted query string
timestamp=1678886400&userId=abc-123

# Append secret + timestamp
timestamp=1678886400&userId=abc-123mysecret1678886400

# Result
Signature: MD5("timestamp=1678886400&userId=abc-123mysecret1678886400")

Reject the request if:

  • Computed signature ≠ Signature header
  • |server_time − timestamp| > 600 seconds (10-minute replay window)

Verification samples

Kotlin
fun verifySignature(body: Map<String, String>, secret: String, received: String): Boolean {
val sorted = body.entries.sortedBy { it.key }.joinToString("&") { "${it.key}=${it.value}" }
val timestamp = body["timestamp"] ?: return false
val raw = "$sorted$secret$timestamp"
val expected = MessageDigest.getInstance("MD5")
.digest(raw.toByteArray())
.joinToString("") { "%02x".format(it) }
return expected == received
}
PHP
function verifySignature(array $body, string $secret, string $received): bool {
ksort($body);
$sorted = http_build_query($body);
$timestamp = $body['timestamp'] ?? '';
return md5($sorted . $secret . $timestamp) === $received;
}
Python
import hashlib
from urllib.parse import urlencode

def verify_signature(body: dict, secret: str, received: str) -> bool:
sorted_body = dict(sorted(body.items()))
timestamp = str(body.get("timestamp", ""))
raw = urlencode(sorted_body) + secret + timestamp
return hashlib.md5(raw.encode()).hexdigest() == received

HMAC-SHA256 — sorted query string, in-game deep link

Used for exactly one flow: the signed deep link in In-Game Top-Up — the URL your own backend builds and hands to your game client to open the embedded top-up page directly, for titles that aren't routing through the Alogame SDK's native call. Every other contract on this site uses one of the two algorithms above; this one only applies to that flow, and it is verified by Alogame, not by you — you only need to build it, using the same secret_key issued for your check_uid/create_order contract.

The signed string is a sorted query string, built the same way as the MD5 contract above — but hashed with HMAC-SHA256 and the result is uppercase hex (unlike the standard contract's lowercase).

Signed fields — sorted alphabetically, joined as key=value&key=value:

FieldRequired?
uidalways
product_idalways
deeplink_tsalways — Unix milliseconds
order_codeonly if your flow uses one
server_idonly for multi-server titles
return_urlonly if you want the player sent back somewhere after payment

Omit a field entirely rather than sending it empty — an optional field that's present changes what gets signed.

deeplink_sig = HMAC-SHA256(secret_key, sorted_query_string(fields)).toUpperCase()

Example — fields { uid: "100002078", product_id: "pkg_100", deeplink_ts: 1755500000000 }:

# Sorted query string
deeplink_ts=1755500000000&product_id=pkg_100&uid=100002078

# Signature
deeplink_sig = HMAC-SHA256(secret_key, "deeplink_ts=1755500000000&product_id=pkg_100&uid=100002078").toUpperCase()

Alogame rejects the link if (for your reference — you can't retry a rejection, so get this right before shipping):

  • Computed signature ≠ deeplink_sig
  • |now − deeplink_ts| > 300000 ms — a 5-minute replay window, narrower than the 10-minute window on the webhook contracts above. Generate deeplink_ts right before opening the link, not ahead of time.
Node.js — building the deep link
const crypto = require("crypto");
const querystring = require("querystring");

function buildDeepLinkSignature(fields, secretKey) {
const sorted = Object.fromEntries(
Object.keys(fields).sort().map((k) => [k, fields[k]])
);
const raw = querystring.unescape(querystring.stringify(sorted));
return crypto.createHmac("sha256", secretKey).update(raw).digest("hex").toUpperCase();
}

const fields = { uid: "100002078", product_id: "pkg_100", deeplink_ts: Date.now() };
const sig = buildDeepLinkSignature(fields, secretKey);
PHP — building the deep link
function buildDeepLinkSignature(array $fields, string $secretKey): string {
ksort($fields);
$raw = urldecode(http_build_query($fields));
return strtoupper(hash_hmac('sha256', $raw, $secretKey));
}

$fields = ['uid' => '100002078', 'product_id' => 'pkg_100', 'deeplink_ts' => (int) round(microtime(true) * 1000)];
$sig = buildDeepLinkSignature($fields, $secretKey);