KorvayneGuides
Backend

Secure telemetry endpoints

Endpoint URLs in shipped clients should be treated as public. Protect the endpoint with server-side validation, not with secrecy in the game client.

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

  1. Player logs in through the game, platform, or studio backend.
  2. Backend verifies the player, entitlement, session, and game build.
  3. Backend issues a short-lived telemetry/access token.
  4. Game passes runtime context and token to Korvayne.
  5. 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.

  1. The player signs in through Steam, Epic, a launcher account, or the studio backend.
  2. The trusted backend verifies that login and creates a short-lived token for this exact game_id, player_id, session_id, and game_build.
  3. The game receives that token with the normal login/session response.
  4. The game calls AC_SetTelemetryToken(token) before telemetry or access checks are sent.
  5. 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"
}
FieldTypeWhen sentHow to treat it
event_idstringAlwaysDeduplicate this value per game/session. It is unique for the local process, not a cryptographic proof.
timestampstringAlwaysUTC client timestamp. Use server receive time for trusted ordering.
game_id, environment, identity_providerstringAlwaysNon-secret routing/context. Empty string is possible if the game did not set it.
player_id, session_id, platform_user_id, game_buildstringWhen enabled in [TelemetryFields]Bind these to the session token on the server. Do not trust them by themselves.
sdk_versionstringWhen enabled in [TelemetryFields]Useful for support and rollout filters.
license_idstringWhen availableShort fingerprint of the license token. Store this, not the raw license.
license_tiernumberWhen availableNumeric product tier from the verified license.
severitystringAlwaysinfo, low, medium, high, or critical.
categorystringAlwaysStable 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, detectionstringAlwaysSensor name. Current payload sends the same value in both fields for compatibility.
confidencenumberAlwaysSignal confidence from 0.0 to 1.0. Correlate before strong punishment.
detailnumberAlwaysSensor-specific numeric detail, such as PID, access mask, or address-like value.
action_takenstringWhen enabled in [TelemetryFields]Current runtime telemetry reports reported. Local termination may happen after the event.
server_observed_ipbooleanWhen enabled in [TelemetryFields]Marker telling your backend to use the request IP it observes. The client does not send an IP value.
client_sends_ipbooleanAlwaysCurrent runtime sends false. Prefer server-observed IP metadata.
paths_redactedbooleanAlwaysWhether path redaction is active for log/telemetry output.
messagestringAlwaysHuman-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

  1. Reject non-HTTPS at the edge.
  2. Enforce request size limits.
  3. Rate-limit by IP and route before deep parsing.
  4. Validate JSON schema.
  5. Validate token signature or server-side session lookup.
  6. Compare request fields to token claims.
  7. Reject duplicate event_id values.
  8. Apply per-player, per-session, and per-build limits.
  9. 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

DimensionStarting pointPurpose
IP + route60/minBlocks broad unauthenticated spam.
token30/minBlocks replay loops for one session.
player_id120/hourLimits compromised or broken player sessions.
session_id300/hourLimits 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.