Core rule
Assume the URL is public, every client field can be forged, and long-lived secrets in the client will be extracted. The server can still validate short-lived tokens, player sessions, platform identity, schemas, rate limits, and replay.
Do not ship backend secrets in anticheat.ini, Unity scripts, Unreal Blueprints, launcher files, or client resources.
Request flow
- Player logs in through the game, platform, or studio backend.
- Backend verifies the player, entitlement, session, and game build.
- Backend issues a short-lived telemetry/access token.
- Game passes runtime context and token to Korvayne.
- Endpoint validates the token and binds request fields to server-side claims.
AC_SetGameId("example-game");
AC_SetPlayerId(player_id);
AC_SetSessionId(session_id);
AC_SetPlatformUserId(platform_id);
AC_SetGameBuild("1.0.42");
AC_SetTelemetryToken(short_lived_token);
Where the runtime token comes from
runtime_session_token means the token is supplied while the game is running. Korvayne does not create this token by itself, and the token should not be hardcoded in anticheat.ini. The studio game/backend creates it after the player has logged in and the backend has verified the account, entitlement, session, and build.
- The player signs in through Steam, Epic, a launcher account, or the studio backend.
- The trusted backend verifies that login and creates a short-lived token for this exact
game_id,player_id,session_id, andgame_build. - The game receives that token with the normal login/session response.
- The game calls
AC_SetTelemetryToken(token)before telemetry or access checks are sent. - Your telemetry/access endpoint validates the token signature or lookup result and rejects requests where the JSON fields do not match the token claims.
// Backend response after your normal login/session check.
{
"player_id": "studio-player-123",
"session_id": "match-789",
"korvayne_token": "short-lived-token-created-by-your-server"
}
// Game startup/session code.
AC_SetPlayerId(player_id);
AC_SetSessionId(session_id);
AC_SetTelemetryToken(korvayne_token);
The fallback auth_token config value is only for local/dev compatibility. Anything shipped in a client config is public, so production builds should use token_source = runtime_session_token.
Telemetry event payload
When [Telemetry] is enabled, Korvayne Runtime sends one JSON object per detection event with Content-Type: application/json. The authorization header name comes from auth_header; in production, the value is the short-lived token previously passed with AC_SetTelemetryToken. AccessCheck uses the same configured header and token.
POST /korvayne/telemetry HTTP/1.1
Content-Type: application/json
Authorization: <short-lived-session-token>
{
"event_id": "7420-123456789-17",
"timestamp": "2026-06-30T12:34:56.123Z",
"sdk_version": "0.1.0",
"license_id": "4f7a9b2c1d0e3a8f",
"license_tier": 1,
"game_id": "example-game",
"environment": "production",
"identity_provider": "steam",
"player_id": "studio-player-123",
"session_id": "match-789",
"platform_user_id": "76561190000000000",
"game_build": "1.0.42",
"severity": "medium",
"category": "handle_checks",
"sensor": "Handle",
"detection": "Handle",
"confidence": 0.75,
"detail": 2035711,
"action_taken": "reported",
"server_observed_ip": true,
"client_sends_ip": false,
"paths_redacted": true,
"message": "handle detection"
}
| Field | Type | When sent | How to treat it |
|---|---|---|---|
event_id | string | Always | Deduplicate this value per game/session. It is unique for the local process, not a cryptographic proof. |
timestamp | string | Always | UTC client timestamp. Use server receive time for trusted ordering. |
game_id, environment, identity_provider | string | Always | Non-secret routing/context. Empty string is possible if the game did not set it. |
player_id, session_id, platform_user_id, game_build | string | When enabled in [TelemetryFields] | Bind these to the session token on the server. Do not trust them by themselves. |
sdk_version | string | When enabled in [TelemetryFields] | Useful for support and rollout filters. |
license_id | string | When available | Short fingerprint of the license token. Store this, not the raw license. |
license_tier | number | When available | Numeric product tier from the verified license. |
severity | string | Always | info, low, medium, high, or critical. |
category | string | Always | Stable grouping such as injection, handle_checks, hook_detection, debugger, boot_state, memory_integrity, sdk_integrity, protected_value, access_check, aim_behavior, savegame_integrity, or enforcement. unknown is a forward-compatibility fallback for future/unmapped sensors. |
sensor, detection | string | Always | Sensor name. Current payload sends the same value in both fields for compatibility. |
confidence | number | Always | Signal confidence from 0.0 to 1.0. Correlate before strong punishment. |
detail | number | Always | Sensor-specific numeric detail, such as PID, access mask, or address-like value. |
action_taken | string | When enabled in [TelemetryFields] | Current runtime telemetry reports reported. Local termination may happen after the event. |
server_observed_ip | boolean | When enabled in [TelemetryFields] | Marker telling your backend to use the request IP it observes. The client does not send an IP value. |
client_sends_ip | boolean | Always | Current runtime sends false. Prefer server-observed IP metadata. |
paths_redacted | boolean | Always | Whether path redaction is active for log/telemetry output. |
message | string | Always | Human-readable evidence summary. If process-name telemetry is disabled, handle events use a generic message. |
enforcement events are emitted when the SDK takes a local action such as closing the protected game. They are controlled by telemetry enablement and min_severity, not by a separate [TelemetryEvents] checkbox.
Schema file: use schemas/telemetry-event.schema.json as the starting contract for your endpoint. Store accepted fields only and ignore unknown future fields.
Reserved fields: module_sha256, module_signer, hardware_id, and raw process_names are config-visible or roadmap fields, but the current runtime detection payload does not emit them as top-level JSON fields.
Access-check payload
When [AccessCheck] is enabled, the endpoint is valid, and the mode is not server_guidance_only, the SDK can POST a startup/recheck request to the studio backend. This is client-side UX guidance only; trusted ban enforcement still belongs on the game or studio server.
{
"request_type": "access_check",
"game_id": "example-game",
"environment": "production",
"identity_provider": "steam",
"player_id": "studio-player-123",
"session_id": "match-789",
"platform_user_id": "76561190000000000",
"game_build": "1.0.42",
"access_provider": "studio_backend",
"mode": "startup_and_recheck",
"sdk_version": "0.1.0",
"license_id": "4f7a9b2c1d0e3a8f",
"license_tier": 1,
"client_side_only": true
}
The access endpoint should answer with a small JSON object:
{ "allowed": true }
{ "allowed": false, "reason_code": "active_ban" }
Schema files: the request contract is available at schemas/access-check-request.schema.json, and the backend response contract is available at schemas/access-check-response.schema.json. The SDK currently reads allowed and, when denied, reason_code.
In drop-in mode, startup denies only close the protected game for on_banned = block_start or terminate. During periodic rechecks, only on_session_ban = terminate closes locally. Other values are emitted/logged so wrapper or game code can show a message, disconnect cleanly, or route the player to support.
Validation order
- Reject non-HTTPS at the edge.
- Enforce request size limits.
- Rate-limit by IP and route before deep parsing.
- Validate JSON schema.
- Validate token signature or server-side session lookup.
- Compare request fields to token claims.
- Reject duplicate
event_idvalues. - Apply per-player, per-session, and per-build limits.
- Store only accepted event fields.
app.post("/korvayne/telemetry", express.json({ limit: "16kb" }), async (req, res) => {
const ev = req.body;
if (typeof ev.event_id !== "string" || typeof ev.timestamp !== "string") {
return res.sendStatus(400);
}
const token = req.get("Authorization");
const session = await verifyTelemetryToken(token);
if (!session || ev.player_id !== session.player_id || ev.game_id !== session.game_id) {
return res.sendStatus(401);
}
if (await seenEvent(ev.event_id)) {
return res.sendStatus(204);
}
await storeTelemetry({
event_id: ev.event_id,
received_at: new Date().toISOString(),
remote_ip: req.ip,
game_id: ev.game_id,
player_id: ev.player_id,
session_id: ev.session_id,
category: ev.category,
severity: ev.severity,
sensor: ev.sensor,
confidence: ev.confidence,
detail: ev.detail,
message: ev.message
});
return res.sendStatus(204);
});
Rate limits
| Dimension | Starting point | Purpose |
|---|---|---|
| IP + route | 60/min | Blocks broad unauthenticated spam. |
| token | 30/min | Blocks replay loops for one session. |
| player_id | 120/hour | Limits compromised or broken player sessions. |
| session_id | 300/hour | Limits noisy match/session loops. |
Access checks
Client access checks are useful for early rejection and cleaner player messaging, but they are not the trusted ban boundary. Multiplayer enforcement should happen on the game server or studio backend before login, matchmaking, server join, inventory, or rewards.
[AccessCheck]
enabled = 1
mode = startup_and_recheck
fail_mode = block
on_banned = block_start
on_session_ban = disconnect
mode = server_guidance_only sends no client access-check request. Use it only when the studio backend or game server already enforces access before login, matchmaking, server join, inventory, or rewards.
