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 contract | Algorithm | Headers |
|---|---|---|
| Web Payment — Standard | HMAC-SHA256 | x-signature, x-timestamp |
| Mobile IAP and Web Payment — Expub | MD5 | Signature |
| In-Game Top-Up — signed deep link | HMAC-SHA256, sorted query string | query 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:
| Header | Value |
|---|---|
x-timestamp | Unix timestamp in milliseconds |
x-signature | HMAC-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.
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| > 600000ms (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 ==.
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'] ?? '';
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)
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:
- Convert the JSON body to query-string format:
key=value&key2=value2 - Sort keys alphabetically
- Append your
secret_key(provided by Alogame — never expose this) - Append the
timestampvalue from the request body (Unix seconds) - 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 ≠
Signatureheader |server_time − timestamp| > 600seconds (10-minute replay window)
Verification samples
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
}
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;
}
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:
| Field | Required? |
|---|---|
uid | always |
product_id | always |
deeplink_ts | always — Unix milliseconds |
order_code | only if your flow uses one |
server_id | only for multi-server titles |
return_url | only 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| > 300000ms — a 5-minute replay window, narrower than the 10-minute window on the webhook contracts above. Generatedeeplink_tsright before opening the link, not ahead of time.
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);
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);