Authentication & signing
Every server-to-server request is signed with your API secret using HMAC-SHA256 — the secret itself never travels over the network.
API keys
An API key has a public key id (kx_live_…, sent in every request) and a secret (kxs_…, used only to sign). Staff with the "Manage API & Webhooks" permission create keys in the backoffice, choose their scopes, IP allowlist, rate limit and expiry, and can rotate the secret or revoke the key at any time.
The secret is displayed once, when the key is created or rotated. Store it in your secrets manager; KinetX staff cannot show it again.
Signing requests
Send three headers with every request:
| X-Api-Key | Your public key id. |
| X-Timestamp | Current Unix time in seconds. Requests more than 300 seconds away from the server clock are refused. |
| X-Signature | Lower-case hex HMAC-SHA256 of the string to sign, keyed with your secret. |
| X-Nonce | Optional. A unique value appended to the string to sign — needed only if you may send byte-identical requests within the same second. |
The string to sign joins four fields with a newline (\n): the HTTP method in upper case, the request URI exactly as sent (path and query string, starting with /api/v1), the timestamp, and the raw body (an empty string for GET). With X-Nonce, the nonce is appended as a fifth line.
POST
/api/v1/operator/players?source=web
1760520600
{"username":"jane_doe",…}
Each signature is accepted once: a captured request cannot be replayed (REPLAYED_REQUEST). Sign the exact bytes you send — serialize the JSON once and use the same string for the signature and the body.
Examples
Register a player with a signed, idempotent POST:
API_KEY="kx_live_your_key_id"
API_SECRET="kxs_your_secret"
URI="/api/v1/operator/players"
BODY='{"username":"jane_doe","email":"jane@example.com","first_name":"Jane","last_name":"Doe","birth_date":"1990-04-12","country":"MT","terms_accepted":true}'
TS=$(date +%s)
# METHOD \n REQUEST_URI \n TIMESTAMP \n BODY → hex HMAC-SHA256 with your secret
SIG=$(printf '%s\n%s\n%s\n%s' "POST" "$URI" "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed 's/^.* //')
curl -sS -X POST "https://kinetx.amhtechnologies.com$URI" \
-H "Content-Type: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "Idempotency-Key: $(uuidgen)" \
--data "$BODY"
<?php
function kinetx(string $method, string $uri, ?array $payload = null, ?string $idempotencyKey = null): array
{
$apiKey = getenv('KINETX_API_KEY'); // kx_live_…
$secret = getenv('KINETX_API_SECRET'); // kxs_…
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$timestamp = (string) time();
$signature = hash_hmac('sha256', strtoupper($method)."\n{$uri}\n{$timestamp}\n{$body}", $secret);
$headers = [
'Accept: application/json',
'Content-Type: application/json',
"X-Api-Key: {$apiKey}",
"X-Timestamp: {$timestamp}",
"X-Signature: {$signature}",
];
if ($idempotencyKey !== null) {
$headers[] = "Idempotency-Key: {$idempotencyKey}";
}
$curl = curl_init('https://kinetx.amhtechnologies.com'.$uri);
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_POSTFIELDS => $body === '' ? null : $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$response = json_decode((string) curl_exec($curl), true);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}", $status);
}
return $response;
}
$player = kinetx('POST', '/api/v1/operator/players', [
'username' => 'jane_doe',
'email' => 'jane@example.com',
'first_name' => 'Jane',
'last_name' => 'Doe',
'birth_date' => '1990-04-12',
'country' => 'MT',
'terms_accepted' => true,
], idempotencyKey: bin2hex(random_bytes(16)))['data'];
$wallet = kinetx('GET', "/api/v1/operator/players/{$player['id']}/wallet")['data'];
import crypto from 'node:crypto';
const HOST = 'https://kinetx.amhtechnologies.com';
export async function kinetx(method, uri, payload, idempotencyKey) {
const body = payload === undefined ? '' : JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', process.env.KINETX_API_SECRET)
.update(`${method.toUpperCase()}\n${uri}\n${timestamp}\n${body}`)
.digest('hex');
const response = await fetch(HOST + uri, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Api-Key': process.env.KINETX_API_KEY,
'X-Timestamp': timestamp,
'X-Signature': signature,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body === '' ? undefined : body,
});
const json = await response.json();
if (!response.ok) throw Object.assign(new Error(json.error.message), json.error);
return json;
}
const { data: player } = await kinetx('POST', '/api/v1/operator/players', {
username: 'jane_doe',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
birth_date: '1990-04-12',
country: 'MT',
terms_accepted: true,
}, crypto.randomUUID());
Scopes
A key may only call endpoints whose scope it holds; otherwise the API answers 403 INSUFFICIENT_SCOPE. Give each integration the smallest set it needs.
| players:read | Read players, their profiles and responsible-gaming limits |
| players:write | Register players, update profiles and status, issue player session tokens |
| limits:write | Set responsible-gaming limits and self-exclusions |
| wallet:read | Read wallet balances and the transaction ledger |
| payments:read | Read deposits, withdrawals and payment methods |
| payments:write | Record paid deposits and create withdrawal requests |
| games:read | Read the game catalogue |
| games:launch | Launch games for players (real and demo) |
| bonuses:read | Read the bonus catalogue and player bonuses |
| bonuses:write | Grant bonuses and redeem promo codes |
| reports:read | Read GGR and financial summaries |
IP allowlist
Restrict a key to the addresses of your servers: single IPv4/IPv6 addresses or CIDR ranges such as 203.0.113.0/24. Calls from other addresses get 403 IP_NOT_ALLOWED. An empty list allows any address.
Player session tokens
Your front end must never hold the API secret. When a player signs in on your site, your server issues a short-lived player session token (POST /api/v1/operator/players/{player}/session-tokens, default 30 minutes, at most 720) and hands it to the browser or app, which calls the player endpoints with Authorization: Bearer kxp_….
// Your server: POST /api/v1/operator/players/1042/session-tokens → { data: { token, expires_at } }
// Your front end (never holds the API secret):
const res = await fetch('https://kinetx.amhtechnologies.com/api/v1/player/games/311/launch', {
method: 'POST',
headers: { Authorization: `Bearer ${playerToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'real', platform: 'mobile' }),
});
const { data } = await res.json();
window.location.href = data.launch_url;
A token only gives access to its own player (profile, balances, transactions, game launch, KinetX Originals). It stops working when it expires or when the API key that issued it is revoked.
Security checklist
- Keep secrets server-side, in a secrets manager — never in front-end code, mobile apps or repositories.
- Use one key per integration and environment, with the fewest scopes and an IP allowlist.
- Rotate secrets periodically and immediately if one may have leaked; revoke keys you no longer use.
- Keep your server clock synchronised (NTP): signatures depend on it.
- Verify the signature of every webhook before trusting it.