Kubian Integrity
Kubian Integrity is an enterprise-grade platform integrity service for Android applications and VR headsets. It combines hardware-backed attestation (TEE and X.509), application binary verification, and real-time environment protection into a single pipeline you can trust before letting a player into your game.
What Kubian Integrity verifies
Every verdict is produced server-side by Static Labs infrastructure. The client never reports its own integrity — it only presents cryptographic proof the server validates. All tokens are encrypted with AXE (Abstract Xpress Encryption) to prevent reverse engineering.
Hardware Verification · TEE / X.509
Attestation keys anchored to Android StrongBox or a Trusted Execution Environment (TEE), presented as a signed X.509 certificate chain rooted at Google Root Certificates.
Application Verification
The package identifier and binary SHA-256 digest are bound into the attestation and cross-checked against the values registered for your project.
Real-time Environment Protection
Continuous detection of root (soft and hard), Frida, Magisk, code injection, debuggers, and anti-debug bypasses — with configurable alert, kick, and ban actions.
Hardware Verification (TEE / X.509)
Attestation keys are generated inside Android StrongBox or a Trusted Execution Environment and presented as a signed X.509 certificate chain rooted at Google Root Certificates. Here is how Kubian validates the hardware state.
When a device runs an attestation flow, Kubian's native layer generates an Elliptic Curve key pair inside the Android KeyStore. On supported devices (Android 9+) the key is generated in StrongBox — a physically isolated secure chip. Otherwise the standard TEE (Trusted Execution Environment) is enforced.
The resulting key is presented as an X.509 certificate chain signed by hardware. The leaf certificate carries the Android Key Attestation ASN.1 extension, which reports hardware-guaranteed values: security level, verified boot state, bootloader lock status, OS patch level, OS version, and the application certificate digest.
On the server, Kubian:
- Validates the full certificate chain signature by-link, verifying each intermediate against its issuer.
- Checks the root certificate against trusted Google Root Certificates (GRCs) and known VR hardware keys.
- Parses the raw ASN.1 attestation extension to read the hardware-reported device state.
- Enforces a minimum OS security patch level (January 2026).
- Cross-references intermediate certificates against the hardware revocation list (CRL).
- Requires verified boot to be Trusted and the bootloader to be locked.
Security levels
| Level | Backing | Description |
|---|---|---|
| Advanced | StrongBox | Key isolated in a separate, highly secure hardware chip. |
| Basic | TEE | Backed by the main processor's secure enclave. Secure. |
| NotTrusted | Software | Generated at software level — a common sign the device or application is compromised. |
Verified boot states
| State | Description |
|---|---|
| Trusted | The OS is official with no immediate threats reported by hardware. |
| Unofficial | The OS has been replaced and is most likely compromised. |
| NotTrusted | The OS has been modified and re-signed by an unknown publisher. |
| Compromised | The OS has been compromised and its security stripped away. |
| Unknown | Kubian could not find information on the OS due to missing or corrupt attestation data. |
Device integrity state
Kubian folds the raw hardware signals — security level, verified boot state,
bootloader lock status, and the device's Widevine security level — into a single
computed string enum carried as the top-level
device_integrity_state field of the integrity token. Your backend does not need
to interpret the individual signals; the computed state is the verdict.
| State | Condition | Meaning |
|---|---|---|
| strong_integrity | Bootloader locked, verified boot Trusted, security level
Advanced/StrongBox, Widevine security level L1.
|
Key isolated in a dedicated hardware security chip and DRM content decoded in secure hardware. The highest integrity state. |
| tee_integrity | Bootloader locked, verified boot Trusted, security level
Basic/TEE, Widevine security level L1.
|
Backed by the main processor's secure enclave (TEE) with hardware-backed Widevine. Secure. |
| simple_tee_integrity | All boot and security conditions passed, but Widevine reported
L2 or L3 instead of L1. StrongBox
devices are demoted to this state too, since a dedicated security chip should
never report L1.
|
The device itself looks genuine, but Widevine could not confirm hardware-level DRM. Rejected unless "Allow Simple Integrity" is enabled. |
| simple_integrity | All boot and security conditions passed, but no Widevine information was
available (null/Unknown).
|
Genuine device with unverifiable Widevine state. Rejected unless "Allow Simple Integrity" is enabled. |
| unmet_integrity | Bootloader unlocked, verified boot not Trusted, an unrecognized
security level, or the attestation itself reported the device as
compromised. |
The device is not running a genuine, untampered OS. Verification fails. |
device_integrity_state enum is derived on Kubian's backend from the
hardware-attested signals — it is never reported by the client. A device that fails
any boot or security condition is always folded into unmet_integrity.
simple_tee_integrity and
simple_integrity during verify_token. Project owners can enable
the "Allow Simple Integrity" toggle under Device checks in API Settings to accept them.
Note that the minimum Security Level still applies: with "TEE integrity and above",
enabled simple states must be at least simple_tee_integrity, and
"Strong integrity only" never accepts simple states. When simple states apply, the
version check runs before any other verification step.
Application Verification
Kubian verifies not only the hardware but also the application running on it. The package identifier and binary digest are bound into the attestation and cross-checked against your project registration.
The attestation extension binds the application's certificate digest and package identifier into the certificate chain. The server decodes these values and produces a recognition verdict for each:
- Application binary: the SHA-256 digest of your application is compared
to the
application-sha256registered on your project. - Package identifier: the package name reported by hardware is compared
to the
project_identifierregistered on your project. - Recognition results:
AppRecognized/AppNotRecognizedandAppIdRecognized/AppIdNotRecognized.
A package identifier is fixed at build time and cannot change without rebuilding the app, so any mismatch is treated as evidence of tampering or an unregistered build.
Real-time Environment Protection
Beyond one-shot attestation, Kubian continuously monitors the running environment with a high-frequency scan loop, certificate-authenticated telemetry, and server-side enforcement decisions.
Architecture overview
When KubianCore.Init() is called, the native library spawns a
detached monitor thread that executes a continuous scan loop at a configurable interval
(default: 2000 ms). Each iteration performs the following in order:
- Hard scan — Frida (5-layer + behavioral + patched-binary + fd-leak), Widevine hook detection, injection, root (hard), root (soft), debugger detection.
- Soft scan — SELinux status, build properties, emulator indicators, busybox, Quest headlock abuse.
- Ban poll (every 10 s) — queries the server for the device's current ban status. If a ban is issued after protection started, the player is kicked immediately.
- Tier recheck (every 60 s) — re-fetches the protection tier from the server. If the tier drops from Max, realtime checks are disabled and only the ban poll continues.
New detections are compared against the previous iteration. A detection that matches the previous result is suppressed — only new or changed detections are reported to the server and acted upon.
Anti-debug armament
Before the scan loop begins, the library arms an anti-attach defense. On aarch64, this is a
raw svc #0 syscall for ptrace(PTRACE_TRACEME), which sets the
kernel's traced flag — any subsequent PTRACE_ATTACH or
PTRACE_SEIZE from another process is rejected by the kernel. This is followed
by prctl(PR_SET_DUMPABLE, 0) to disable core dumps and prevent
/proc/[pid]/mem reads. The same pattern is used on arm32 and x86_64 with
their respective syscall numbers. All three architectures use raw syscall instructions that
bypass userland function hooks.
Detection checks
Frida detection (frida)
Five-layer Frida detection:
- Scans
/proc/self/mapsfor 12 Frida library signatures:frida,gum-js,gadget,linjector,frida-agent,libgadget,frida-inject,frida-gadget,gmain,gdbus,frida-server,re.frida. - Detects non-system RWX memory segments — Frida injects its agent as executable code.
- Reads
/proc/self/environforFRIDA_environment variables that leak Frida's presence. - Probes 17 filesystem paths including
/data/local/tmp/,/data/adb/, system paths, and renamed variants (fs-server,.frida). - Checks
/proc/net/tcpand/proc/net/tcp6for default ports 27042/27043, and/proc/self/cmdlinefor frida in arguments.
Injection detection (injection)
Two-pronged injection detection:
- Scans
/proc/self/mapsfor 36 framework signatures:substrate,xposed,riru,zygisk,lsposed,edxposed,shadowhook,dobby,libwhale,sandhook,shizuku,kernelsu,ksud,riru_core,zygisk_next,liblsplant,libepoxy,spirit,hideroot,magiskhide,blink,libmemtraces,frida_gadget,libfrida,libxposed_art,libnativehook,liblspd,libhpicore,libjiagu,libDexHelper,libexecmain,libchaosvmp,libjiagu_art,libtuplus,libprotect,libapp_shield. - Detects executable
r-xpmappings from/data/local/tmp/(injected native payloads) and RWX segments in/data/excluding/data/app/and/data/dalvik-cache/.
Root — hard (root_hard)
Six-pronged hard root detection. A single hard signal is conclusive.
- Filesystem probe: checks 47 known su/root paths including Magisk, KernelSU, APatch, ZygiskSU, Shamiko, LSPosed, TrickyStore, and init .rc scripts.
- UID check: calls
getuid()— if the process runs as uid 0, it has root. - Mount parsing: reads
/proc/self/mountinfoformagisk,KSU,kernelsu,/data/adb,ksu,zygisk,rirumount entries. - Hidden root in maps: scans
/proc/self/mapsfor 12 hidden root module libraries (zygisk,riru,lsposed,magisk,kernelsu,ksud,magiskhide,shamiko,zygisk_next,riru_core,liblsposed,lspd). - Command-line: reads
/proc/self/cmdlineformagiskorksuin process arguments. - Module enumeration: enumerates
/data/adb/modules/via raw getdents syscall looking for su binaries inside module directories.
Root — soft (root_soft)
Multi-layered soft root and emulator detection:
- Reads
/sys/fs/selinux/enforce—0means permissive. - Checks
ro.build.tagsfortest-keysanddev-keys. - Checks
ro.securefor0(insecure boot). - Checks
ro.debuggablefor1. - Checks
ro.build.typeforenganduserdebug. - Checks
ro.adb.securefor0(unauthorized adb). - Reads
ro.boot.selinuxfor permissive. - Full emulator detection: 15 emulator keywords (
goldfish,ranchu,bluestacks,nox,ldplayer,memu,genymotion, etc.) checked against 6 property names. - Filesystem checks:
/dev/socket/qemud,/dev/qemu_pipe,/system/lib/libc_malloc_debug_qemu.so,/sys/qemu_trace. - Reads
/proc/cpuinfofor emulator CPU signatures. - Checks
/proc/tty/driversfor goldfish. - Detects busybox in maps and filesystem.
Debugger detection (debugger)
Four-layer debugger detection:
- Reads
/proc/self/statusand parsesTracerPid— non-zero means a debugger is attached via ptrace. - Enumerates ALL threads in
/proc/self/task/via raw getdents syscall and checksTracerPidfor each thread — catches debuggers that attach to individual threads rather than the main process. - Scans
/proc/self/mapsfor 13 debugger artifacts:liblldb.so,libgdbserver.so,android_server,android_server64,android_server32,libthread_db.so,libpython,JDWP,jdwp,libptrace,idaq,id64,x64dbg. - Reads
/proc/self/wchanforptraceandn_tracerpidkernel wait channel strings.
Anti-debug trap (antidebug)
Arm the ptrace/seccomp trap that blocks debugger attachment. This check is not a detection — it is the defensive armament itself. If the anti-debug armament has been bypassed or stripped, this check fires as a detection signal.
Headlock abuse (headlock_abuse)
Quest-specific detection for head-lock hijacking used to bypass guardian boundaries or perform injection via the head-tracking subsystem:
- Reads the
debug.oculus.headlocksystem property — values1(active) or3(hijacked) indicate headlock abuse is engaged. - Reads
debug.oculus.headlock.translation.zto measure the Z-axis displacement — values outside ±10.0 indicate the headset is being forcibly repositioned to bypass spatial restrictions.
Frida behavioral detection (frida_behavioral)
Memory-mapping anomaly detection that catches patched Frida builds:
- Counts anonymous
r-xp(read-execute) memory mappings in/proc/self/maps. Normal Android apps user--pfor anonymous memory;r-xpindicates code injection. - Triggers if the count exceeds 3 anonymous
r-xpsegments — catches Frida's Gum code allocator pages even when--strict-wxis active and library names are stripped.
This layer defeats frida-bin-patcher and similar tools that rename binary symbols and strip debug info but cannot eliminate the anonymous executable mappings required for code injection.
Frida patched-binary detection (frida_module)
Module name and command-line scanning for renamed/patched Frida builds:
- Scans
/proc/self/mapsfor 5 patched-binary signatures:florida,oemcodec,sysmon,libflorida,liboemcodec. - Scans
/proc/[pid]/cmdlineacross all processes forcom.android.systemuidisguised Frida servers — catches MagiskHluda/Florida and phantom-frida that masquerade as system processes.
This layer defeats tools that rename frida-server to florida,
oemcodec, or sysmon and launch it under a system UI process
name.
Frida fd-leak detection (frida_memfd)
File descriptor symlink analysis that catches in-memory Frida injection:
- Enumerates
/proc/self/fdvia rawgetdents64syscall. - Reads each symlink target via raw
readlinkatsyscall (bypasses userland hooks). - Triggers if any fd symlink contains
fridaorlinjector, or if pipe targets match thepipe-XXXXXXpattern used by Frida's inter-thread communication.
This layer defeats undetected-frida and other approaches that load Frida via
memfd_create() — the injection leaves file descriptors that cannot be
hidden from /proc/self/fd enumeration.
Widevine hook detection (widevine_hook)
Four-layer Widevine integrity detection:
- Format validation: verifies
widevine_idis a 64-character hex string — rejects synthetic, replayed, or malformed IDs. - Entropy check: counts unique hex characters — flags low-entropy IDs that indicate brute-forced or generated values.
- Consistency check: re-fetches Widevine ID every 5 scan cycles and compares with cached value — catches runtime hooks returning spoofed values.
- GOT/PLT hook detection: scans
/proc/self/mapsfor RWX permissions inlibmediadrmbase.so,libmedia.so,libutils.so,libmediautils.so, andlibmediandk.so— detects inline hooks on MediaDrm JNI functions used to intercept or modify Widevine ID retrieval.
Default actions
| Check | Detects | Default action |
|---|---|---|
frida |
Frida instrumentation framework | Alert |
injection |
Code / library injection into the process | Alert |
root_hard |
Persistent / privileged root (e.g. Magisk full install) | Ban |
root_soft |
Soft or hidden root indicators, emulator environment | Kick |
debugger |
Attached debuggers (ptrace, JDWP, LLDB) | Kick |
antidebug |
Anti-debug bypass attempts | Alert |
headlock_abuse |
Quest headlock hijacking for spatial bypass | Kick |
frida_behavioral |
Anonymous executable memory (patched Frida builds) | Kick |
frida_module |
Renamed/patched Frida binaries (florida, oemcodec, sysmon) | Kick |
frida_memfd |
In-memory Frida injection via fd leaks | Kick |
widevine_hook |
MediaDrm function hooking, invalid/tampered Widevine IDs | Ban |
Available actions are alert, kick, and ban. The set of
checks and their actions are configurable per project in the Kubian dashboard (Max tier).
Ban enforcement
Every 10 seconds the monitor thread queries the server for the device's ban status. The server validates the device certificate, checks the ban database, and returns the current state.
- If the device was not banned when protection started and a ban is
detected during the session, the player is kicked immediately and the server records a
REALTIME_BANNED_WHILE_PLAYINGaudit event. - If the device was already banned when protection started, the ban poll does not kick — this prevents repeated kicks on rejoin. The developer's attestation backend is expected to handle the ban at login time instead.
Tier recheck
Every 60 seconds the library re-fetches the protection tier from the server. If the tier drops from Max to a lower tier, realtime detection checks are disabled and only the ban poll continues. The server signs the tier response with RSA; the client rejects any unsigned or tampered response and falls back to full Max-tier protection.
Server-side enforcement
Detection logic runs on the client, but the enforcement decision is made server-side and
cannot be forged. When a detection fires, the library sends a telemetry report to the
server (authenticated via the device certificate). The server evaluates the configured
action for that check and responds with a directive: note (log only),
kick (remove from session), or an executed ban (permanent device-level
restriction).
Token Encryption — AXE
Every challenge token and integrity token issued by Kubian is wrapped in AXE (Abstract Xpress Encryption) — a lightweight, server-side encryption layer that prevents clients and intermediaries from reading or reverse-engineering token contents.
Why AXE exists
Without AXE, integrity tokens are signed JWTs — base64-encoded JSON that anyone can decode and read. Attackers who inspect token payloads can study field names, claim structures, and attestation details to find weaknesses or craft targeted bypasses.
AXE solves this by encrypting the entire token before it leaves the server. The result is
an opaque blob that only the server can decrypt and verify. The client sees
AXE.{encrypted}.{signature} — no payload fields, no structure, no
attack surface.
How AXE works
AXE combines two NIST-standard primitives into a single operation:
- AES-256-GCM — authenticated encryption that provides confidentiality and integrity in one pass. Each token gets a unique 12-byte nonce, making identical payloads produce different ciphertexts every time.
- HMAC-SHA256 — a separate MAC key verifies the encrypted blob hasn't been tampered with before decryption is attempted. This prevents chosen-ciphertext attacks on the AES-GCM layer.
Both keys are derived from a single 256-bit server key using SHA-256 key derivation. The AES key and MAC key are never the same value, even though they originate from the same source.
Token format
AXE.{base64url(nonce + AES-GCM-ciphertext + tag)}.{base64url(HMAC-SHA256-signature)}
| Component | Size | Description |
|---|---|---|
AXE. |
4 bytes | Prefix identifying the token as AXE-encrypted. |
| Encrypted blob | Variable | 12-byte nonce + AES-256-GCM ciphertext + 16-byte authentication tag, encoded in a custom base64 alphabet. |
| HMAC signature | ~43 bytes | SHA-256 MAC over the entire encrypted blob, encoded in URL-safe base64. |
Seal (encrypt)
axe_token = AXE.seal(payload_dict, server_key)
The payload dictionary is serialized to compact JSON, encrypted with AES-256-GCM using a
random nonce, and signed with HMAC-SHA256. The result is the AXE.{blob}.{sig}
string returned to the client.
Open (decrypt)
payload_dict = AXE.open(axe_token, server_key)
The HMAC signature is verified first. If it fails, the token is rejected immediately without attempting decryption. If it passes, the encrypted blob is decrypted with AES-256-GCM and the resulting JSON is parsed back into a dictionary.
How AXE secures the integrity flow
The attestation pipeline uses AXE as an outer encryption shell around every token:
| Endpoint | What happens |
|---|---|
get-challenge-token |
The server creates a signed JWT challenge token, then wraps it in AXE before
returning it to the client. The client receives an opaque
AXE.{...} string.
|
get-integrity-token |
The server decrypts the AXE-wrapped challenge token, extracts and verifies the inner JWT, then creates a new signed integrity JWT — and wraps that in AXE before returning it. |
verify_token |
Your backend forwards the AXE token to Kubian. The server decrypts the AXE layer, extracts the inner JWT, verifies the PS256 signature, and evaluates the attestation claims. |
What AXE prevents
- Payload inspection — clients and intermediaries cannot read token
fields, claim names, or attestation data. The only visible text is the opaque
AXE.prefix. - Token replay with modification — any change to the encrypted blob invalidates both the AES-GCM authentication tag and the HMAC signature.
- Cross-protocol analysis — different server keys produce completely different ciphertexts for identical payloads, making cross-project token analysis impossible.
- Structural reverse-engineering — attackers cannot learn field names, data types, or claim structure from intercepted tokens.
Developer impact
None. The AXE encryption and decryption are handled entirely by the Kubian
SDK and server infrastructure. Your client code receives and forwards opaque
AXE.{...} strings exactly as it would any other token. Your backend calls
verify_token with the AXE token in the request body — Kubian handles the
decryption internally.
Overview & Authentication
Technical reference for the attestation endpoints: base URL, authentication
headers, request payloads, and the complete JSON response schemas produced by
verify_token.
Base URL
https://api.kubian.app
All endpoints require JSON request bodies and expect the Content-Type:
application/json header. Responses are always JSON.
Authentication — ACT (Access Content Token)
Every attestation and ban API call is gated by an ACT — Access
Content
Token for Applications and User Accounts. It is not a JWT; it is a structured
credential
string sent in the access-token header:
access-token: KB|{project-app-id}|{project-app-secret}
- Format validation: the header must begin with
KB|and split into exactly three pipe-delimited segments. - Project lookup: the parsed
app-id/app-secretpair is matched against the ACT database. - Standing check: suspended or payment-paused projects are rejected before any attestation logic runs.
app-secret in the Unity client or any on-device code. All API
calls must originate from your secure backend.
ACT error reference
| Error code | HTTP | Cause |
|---|---|---|
ACT_Token_Missing |
400 | The access-token header was not present. |
ACT_Token_Format_Invalid |
400 | The header did not split into exactly three segments or did not start with
KB.
|
ACT_Token_Invalid_ID |
404 | The app-id / app-secret pair did not match any
project. |
Verify Integrity Token
Verifies an AXE-encrypted attestation token generated on a device. This is the endpoint your backend calls to obtain a device verdict. Tokens are single-use and expire after 10 minutes.
Endpoint
https://api.kubian.app/platform_integrity/verify_token
Required headers
| Header | Value | Description |
|---|---|---|
access-token |
KB|{app-id}|{app-secret} |
ACT credential for your project. |
Content-Type |
application/json |
Must be set to JSON. |
JSON body
| Field | Type | Description |
|---|---|---|
token |
string | The AXE-encrypted attestation token passed from the client to your backend, then forwarded here. The server decrypts the AXE layer internally and verifies the inner JWT signature. |
{
"token": "AXE.tJZxIceqye1-DYr4vHuX221ZMiOOC2ngieUhZdc42Ke..."
}
Success response
On successful verification the response carries success: true with
tee_integrity, strongbox_integrity,
app_integrity, and a token field containing the AXE-decrypted
inner JWT. See Success
Responses for the full reference.
token field with the raw
AXE-decrypted inner JWT (not decoded, not re-signed). This is useful for debugging,
logging, or forwarding to downstream services.
Verify Integrity Token — Success Responses
When verification passes all checks the response carries
success: true with three integrity fields that indicate the device's
hardware attestation tier and application verification state.
TEE-backed device
{
"success": true,
"tee_integrity": "MEETS_TEE_INTEGRITY",
"strongbox_integrity": "UNSUPPORTED",
"app_integrity": "MEETS_APP_INTEGRITY",
"nonce": "Your-Secure-Server-Nonce",
"unique_id": "device-unique-id",
"token": "eyJhbGciOiJQUzI1NiIs..."
}
StrongBox-backed device
{
"success": true,
"tee_integrity": "MEETS_TEE_INTEGRITY",
"strongbox_integrity": "MEETS_STRONGBOX_INTEGRITY",
"app_integrity": "MEETS_APP_INTEGRITY",
"nonce": "Your-Secure-Server-Nonce",
"unique_id": "device-unique-id",
"token": "eyJhbGciOiJQUzI1NiIs..."
}
| Field | Values | Meaning |
|---|---|---|
tee_integrity |
MEETS_TEE_INTEGRITY |
Bootloader locked, verified boot trusted, TEE-backed attestation passed. |
strongbox_integrity |
MEETS_STRONGBOX_INTEGRITY | UNSUPPORTED |
MEETS_STRONGBOX_INTEGRITY when the device has a StrongBox
security chip (Advanced security level). UNSUPPORTED when only
TEE is available (Basic security level). |
app_integrity |
MEETS_APP_INTEGRITY |
Application SHA-256 and package identifier match registered values. |
token |
string | The AXE-decrypted inner JWT. This is the raw signed JWT extracted from the AXE envelope — not decoded, not re-signed. Useful for debugging, logging, or passing to downstream services. |
strongbox_integrity to apply different policies per security tier. For
example, MEETS_STRONGBOX_INTEGRITY may unlock higher-value in-game actions
while UNSUPPORTED permits standard gameplay.
Verify Integrity Token — Failure Responses
Every failure from verify_token returns a structured
JSON body. Map the returned status to the appropriate client handling with the reference
below.
| Scenario | HTTP | Response body |
|---|---|---|
| Request Validation | ||
| Missing JSON body | 400 | {"success": false, "status": "BAD_REQUEST", "message": "Invalid request. Missing 'json' body."} |
Missing token in body |
400 | {"success": false, "status": "BAD_REQUEST", "message": "Invalid request. Missing 'token' in json body."} |
| Device provided no application information | 400 | {"success": false, "status": "FAILURE_ERROR: DEVICE DID NOT PROVIDE APPLICATION INFORMATION", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Authentication (ACT) | ||
Missing access-token header (ACT) |
400 | {"success": false, "status": "BAD_REQUEST", "db": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| Malformed ACT | 400 | {"success": false, "status": "BAD_REQUEST", "db": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Format_Invalid"} |
| Unknown project | 404 | {"success": false, "status": "UNKNOWN_PROJECT", "db": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
| Token Validation | ||
| Token expired | 401 | {"success": false, "status": "INTEGRITY_TOKEN_EXPIRED", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Token signature invalid | 401 | {"success": false, "status": "INTEGRITY_TOKEN_INVALID", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Token missing segments | 500 | {"success": false, "status": "INTEGRITY_TOKEN_STANDARDS_UNMET", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Integrity token replayed / consumed or malformed | 401 | {"success": false, "status": "INTEGRITY_TOKEN_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Challenge token replayed / consumed | 401 | {"success": false, "status": "CHALLENGE_TOKEN_ALREADY_CONSUMED", "unique_id": "device-unique-id"} |
| Unknown / unhandled decode error | 400 | {"success": false, "status": "UNKOWN_ERROR"} |
| Device Attestation | ||
| Device integrity compromised (rooted, hooked) | 403 | {"success": false, "status": "DEVICE_INTEGRITY_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Attested serial revoked (X.509 CRL) | 403 | {"success": false, "status": "DEVICE_ATTESTED_REVOKED", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Device issuer not allowed (Google / Meta gating) | 403 | {"success": false, "status": "DEVICE_ATTESTED_DISALLOWED", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Bootloader / verified boot integrity failed | 403 | {"success": false, "status": "DEVICE_INTEGRITY_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Integrity standard unmet (security level) | 403 | {"success": false, "status": "DEVICE_INTEGRITY_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Device OS outdated | 403 | {"success": false, "status": "DEVICE_INTEGRITY_UPDATE_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Application SHA-256 or package mismatch | 403 | {"success": false, "status": "APP_INTEGRITY_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Application version below minimum | 403 | {"success": false, "status": "APP_VERSION_STANDARDS_UNMET", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Project Authorization | ||
| Device banned | 403 | {"success": false, "status": "DEVICE_AUTHORIZATION_FAILED", "unique_id": "device-unique-id", "token": "eyJhbGciOiJQUzI1NiIs..."} |
| Project suspended | 403 | {"success": false, "status": "PROJECT_SUSPENDED", "message": "Project has been suspended by administrator."} |
| Project payment past due | 402 | {"success": false, "status": "PROJECT_PAUSED", "message": "Project payment is past due. Please pay for your project for the services to be put back online."} |
integrity_token is atomically consumed the first time it is verified.
Submitting the same token again returns INTEGRITY_TOKEN_STANDARDS_UNMET with
HTTP 401. Every response (success or failure) includes a token field containing
the AXE-decrypted inner JWT for debugging and downstream use.
Hardware Revoked List
Retrieves the current hardware revocation list, sourced from Google's publicly available Attestation CRL (Certificate Revocation List) and extended with any hardware Kubian has permanently suspended. Entries are keyed by intermediate hardware certificate serial numbers.
Data source — Google's public Attestation CRL
Kubian mirrors
https://android.googleapis.com/attestation/status, the revocation list Google
publishes for attested hardware. Certificates Google has listed as revoked are treated as
permanently revoked hardware. The response keys are the serial numbers of the revoked
intermediate hardware certificates, and each entry carries the status and
reason Google publishes (for example REVOKED /
KEY_COMPROMISE).
Endpoint
https://api.kubian.app/platform_integrity/hardware-intermediate/revoked
Response schema — 200 OK
{
"entries": {
"a1b2c3d4e5f6g7h8i9j0": {
"status": "REVOKED",
"reason": "KEY_COMPROMISE"
}
}
}
The Flow at a Glance
How to start the attestation flow from the client / application side using the native Kubian libraries, and how to complete verification on your own backend.
The attestation pipeline requires 4 API calls total: 2 initiated by the SDK (Init and GetToken) and 2 internal calls the SDK performs automatically during the token request.
| # | API Call | Initiated By | Purpose |
|---|---|---|---|
| 1 | get-nonce |
SDK (during Init) | Fetch a signed hardware nonce and create a device identity. |
| 2 | get-device-certificate |
SDK (during Init) | Fetch the device attestation certificate, HW token, and entitlement status. |
| 3 | challenge |
SDK (automatic) | Exchange the device cert for a signed challenge token (AXE-encrypted). |
| 4 | integrity |
SDK (automatic) | Mint a signed integrity token from the challenge token and hardware proof (AXE-encrypted). |
- Core Initialization — call
KubianCore.Init(appId, callback). The SDK fetches the nonce, attestation certificate, and entitlement status in a single handshake. Always check the callback'serrorstring before continuing. - Request Integrity Token — call
KubianIntegrity.getToken(appId, nonce)with a server-generated nonce. The SDK performs the challenge and integrity minting internally and returns a signedintegrity_token. - Submit to your backend — forward the token to your
own
server, which calls
verify_tokenand asserts thenonce.
KubianIntegrity.getToken) handle this
exchange natively — never call those endpoints from your own code.
Core Initialization
Initializing the Kubian core from C# / Unity. A single
KubianCore.Init call fetches the attestation certificate, HW token, nonce, and
entitlement status in one handshake (2 internal API calls). Always handle the callback
error string.
using KubianCore;
using UnityEngine;
public class KubianBootstrapper : MonoBehaviour
{
private const string AppId = "YOUR_KUBIAN_APP_ID";
void Start()
{
InitializeCore();
}
private void InitializeCore()
{
Debug.Log("[Kubian] Initializing Core Platform...");
KubianCore.Init(AppId, (error) =>
{
if (!string.IsNullOrEmpty(error))
{
Debug.LogError("[Kubian] Core Initialization Failed: " + error);
HandleFailure("Failed to initialize security engine.");
return;
}
Debug.Log("[Kubian] Core Initialized. Ready for Attestation.");
ProceedToAttestation();
});
}
private void HandleFailure(string reason)
{
// Kick the player to the main menu or prevent entering multiplayer matches.
}
private void ProceedToAttestation()
{
// Generate a challenge_nonce on your server, then call KubianIntegrity.getToken().
}
}
Requesting the Integrity Token
Generate a server-side challenge nonce and request the integrity token from the native Kubian attestation library. The SDK handles the challenge and integrity minting internally (2 additional API calls).
using KubianIntegrity;
using System.Security.Cryptography;
// The "challenge_nonce" must be a URL-safe Base64 string generated by your
// application server, between 22 and 172 characters long.
string GetChallengeNonceFromAppServer()
{
byte[] randomBytes = new byte[16];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(randomBytes);
}
string base64Nonce = Convert.ToBase64String(randomBytes);
return base64Nonce.Replace('+', '-').Replace('/', '_');
}
void RequestAttestation()
{
string challenge_nonce = GetChallengeNonceFromAppServer();
KubianIntegrity.getToken(AppId, challenge_nonce)
.OnComplete((token, error) =>
{
if (!string.IsNullOrEmpty(token))
{
StartCoroutine(SubmitToBackend(token));
}
else
{
if (AuthenticationStatus != null) AuthenticationStatus.text = error;
isAuthenticating = false;
}
});
}
Backend Verification
Your backend receives the token from the client and
forwards it to Kubian with your ACT header. The successful response returns a
nonce.
POST https://api.kubian.app/platform_integrity/verify_token
Content-Type: application/json
access-token: KB|{project-app-id}|{project-app-secret}
{
"token": "{AXE-encrypted-integrity-token-from-client}"
}
nonce. Your backend must verify that this returned nonce matches the
exact nonce you assigned to the client session. If it matches, the client is verified. If it
does not match, reject the client immediately to prevent replay attacks. Never put your
app-secret in the Unity client.
Best Practices
Operational guidance for building a resilient, replay-safe attestation flow into your game or application.
- Secure nonces: generate cryptographically secure, unpredictable nonces on your server — never sequential numbers.
- Expiration tracking: track nonce and token expiration to prevent replays.
- Re-verification: recheck attestation on high-stakes actions — joining/leaving rooms, trading, purchasing, entering competitive modes.
- Don't block the main thread: callbacks are asynchronous; avoid
Thread.Sleepor blocking loops. - Retry logic: on network timeouts during Init, back off and retry before disconnecting the user.
- Obfuscation: run Unity assemblies (especially success callbacks) through an obfuscator such as IL2CPP with string encryption.
Device Ban API Reference
Manage and enforce hardware-level device bans for your project. Ban devices by their hardware-bound unique ID, query ban state, and revoke bans when necessary.
access-token header
in
the form KB|{project-app-id}|{project-app-secret}. See the
ACT error reference
for failure responses.
ban-id values on their own database if they wish to unban a device later. The
ban-id is returned only once at ban execution and cannot be retrieved from
Kubian
later. See the Ban Requirements pages.
Ban Device
Bans a specific device from accessing your application. The device's
unique_id must exist in your project's associated device list before a ban can
be
placed.
Endpoint
https://api.kubian.app/platform_integrity/ban_device
Headers
| Header | Value |
|---|---|
access-token |
KB|{project-app-id}|{project-app-secret} |
Content-Type |
application/json |
JSON body
| Field | Type | Description |
|---|---|---|
unique_id |
string | The unique device identifier generated by the client and returned in attestation responses. |
ban_time_in_hours |
int | Optional. Duration of the ban in hours (default 168, capped at 8760 = 1 year). Set to 0 for a permanent ban that will never expire until manually revoked. |
reason |
string | Optional. Offending ban reason (max 500 characters). Omitted if not provided. |
Responses
| Status | Scenario | Response body |
|---|---|---|
| 200 | Success | {"message": "Device Device-Unique-ID: has been banned", "ban-id": "ban_id_12345678abcdef"} |
| 401 | Missing / malformed ACT | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| 404 | ACT pair not found | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
| 400 | Missing or invalid JSON body / unique_id |
{"message": "Missing unique_id"} or {"message": "Missing JSON body"} |
| 400 | Invalid ban_time_in_hours |
{"message": "ban_time_in_hours must be a non-negative integer"} |
| 404 | Project has no associated devices | {"message": "Failed to ban Device-Unique-ID: Project has no associated devices"} |
| 404 | Unique ID not associated with the project | {"message": "Failed to ban Device-Unique-ID: Unique ID not found in project list"} |
| 409 | Device already banned under an active policy | {"message": "Device Device-Unique-ID is already banned"} |
Unban Device
Revokes an active ban on a device using the device's
unique_id, the unique ban_id receipt generated when the ban was
created, or both. Providing either identifier alone is sufficient to unban. A stored
ban_id lets you revoke a ban even after the device's unique_id has
rotated or expired.
Endpoint
https://api.kubian.app/platform_integrity/unban_device
Headers
| Header | Value |
|---|---|
access-token |
KB|{project-app-id}|{project-app-secret} |
Content-Type |
application/json |
JSON body
| Field | Type | Description |
|---|---|---|
unique_id |
string | The unique device identifier being unbanned. Optional if a valid
ban_id is supplied. Works even if the unique_id has since rotated
or expired.
|
ban_id |
string | The one-time ban receipt ID (with or without the ban_id_ prefix).
Optional if a matching unique_id is supplied.
|
Responses
| Status | Scenario | Response body |
|---|---|---|
| 200 | Success | {"message": "Device Device-Unique-ID: has been unbanned", "purged-ban-id": "ban_id_12345678abcdef"} |
| 401 | Missing / malformed ACT | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| 404 | ACT pair not found | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
| 400 | Neither unique_id nor ban_id supplied |
{"message": "Missing unique_id or ban_id (at least one is required)"} |
| 404 | No ban record matches the supplied identifier | {"message": "Failed to unban Device-Unique-ID: no ban found for this unique_id"} or {"message": "Failed to unban ban_id_12345678abcdef: ban record not found"} |
List Banned Devices
Retrieves an object containing all currently active hardware bans linked to
your project. Expired bans are omitted automatically, and ban-id values are
masked
to their first 5 hex characters to minimize data exposure.
Endpoint
https://api.kubian.app/platform_integrity/user/list_bans
Headers
| Header | Value |
|---|---|
access-token |
KB|{project-app-id}|{project-app-secret} |
Content-Type |
application/json |
JSON body
No request body parameters are necessary for this query.
Responses
| Status | Scenario | Response body |
|---|---|---|
| 200 | Success | {"entries": {"unique-id-example-1": {"ban-id": "ban_id_12345...", "ban_time_remaining": 167.95}}} |
| 401 | Missing / malformed ACT | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| 404 | ACT pair not found | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
Device Ban Status
Queries the operational ban state of a hardware unique ID within your project. If the device has an active restriction, deployment timestamps and remaining hour timelines are returned.
Endpoint
https://api.kubian.app/platform_integrity/user/device_ban_status
Headers
| Header | Value |
|---|---|
access-token |
KB|{project-app-id}|{project-app-secret} |
Content-Type |
application/json |
JSON body
| Field | Type | Description |
|---|---|---|
unique_id |
string | The hardware unique device identifier to search against active policy registers. |
Responses
| Status | Scenario | Response body |
|---|---|---|
| 200 | Device cleanly authorized | {"banned": false, "message": "Device Device-Unique-ID: No active bans found for this Unique ID.", "success": true} |
| 200 | Device matches active ban policy | {"details": {"ban_id": "ban_id_12345", "ban_time_remaining_hours": 167.95, "banned_at": "2026-05-21T16:53:27", "expires_at": "2026-05-28T16:53:27", "unique_id": "Device-Unique-ID"}, "banned": true, "success": true} |
| 401 | Missing / malformed ACT | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| 404 | ACT pair not found | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
| 400 | Missing unique_id |
{"message": "Missing unique_id"} or {"message": "Missing JSON body"} |
| 404 | Project has no associated devices | {"message": "Failed to find ban status for Device-Unique-ID: No associated devices found for this project"} |
| 404 | Unique ID does not match any associated device | {"message": "Failed to find ban status for Device-Unique-ID: Unique ID does not match any of the associated devices with this project"} |
Get Ban Details
Queries granular tracking metrics and timestamps for a specific restriction record. This endpoint strictly requires the exact, full unmasked ban receipt ID. Masked or truncated entries are rejected to eliminate lookup guessing.
Endpoint
https://api.kubian.app/platform_integrity/user/ban_details
Headers
| Header | Value |
|---|---|
access-token |
KB|{project-app-id}|{project-app-secret} |
Content-Type |
application/json |
JSON body
| Field | Type | Description |
|---|---|---|
ban_id |
string | The exact, full unmasked ban identifier received on creation (e.g.
ban_id_12345678abcdef). Partial or masked tokens are rejected.
|
Responses
| Status | Scenario | Response body |
|---|---|---|
| 200 | Ban profile found | {"success": true, "details": {"unique_id": "Device-Unique-ID", "ban_id_masked": "ban_id_12345", "ban_id_full": "ban_id_12345678abcdef", "banned_at": "2026-05-21T16:53:27", "expires_at": "2026-05-28T16:53:27", "is_active": true, "ban_time_remaining_hours": 167.95}} |
| 401 | Missing / malformed ACT | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Missing"} |
| 404 | ACT pair not found | {"graph": "Access Content Token - Failed", "message": "Unable to verify your Kubian 'access-token'. Error: ACT_Token V2.0 - ACT_Token_Invalid_ID"} |
| 400 | Missing ban_id |
{"message": "Missing ban_id"} or {"message": "Missing JSON body"} |
| 404 | No matching ban record | {"success": false, "message": "Failed to find ban details: Invalid or unrecognized ban_id for this project."} |
Device Ban Requirements & Compliance
What you, as a developer, are responsible for tracking when using the
Device
Ban API — including ban-id receipts, device identifiers, and the
compliance
terms that govern ban enforcement.
Tracking ban-id receipts
Bans are a one-time receipt. When ban_device succeeds, the
response contains a ban-id value:
{
"message": "Device Device-Unique-ID: has been banned",
"ban-id": "ban_id_12345678abcdef"
}
unique_id while that identifier is unrotated, but after the
unique_id rotates the ban-id becomes the only reliable way to
revoke
the ban. Store the ban-id immediately after a successful ban response.
- Persist the
ban-idimmediately after a successful ban response. - Store it together with the device's
unique_idand the ban timestamps. - Treat
ban-idas a secret credential for the unban flow — it can revoke the ban. - Pass either the
unique_id, theban-id, or both tounban_device. Use the full unmasked value withban_details; masked values are rejected.
Device Identifiers
Every attested device is identified by a unique_id, a
hardware-bound identifier generated by the client and returned in attestation responses. It
is
the key your project associates with a device, and the value you pass to every ban endpoint.
| Identifier | Source | Notes |
|---|---|---|
unique_id |
Attestation responses | Required by ban_device, device_ban_status, and, along
with ban_id, optional for unban_device (either
identifier alone unbans). Rotates every 60 days.
|
ban_id |
Created at ban time | One-time receipt. Required by ban_details and, along with
unique_id, optional for unban_device (either
identifier alone unbans).
|
Rotation & privacy: unique_id values rotate every 60 days
by
design. Active bans persist across rotation — Kubian links the ban to the device's
hardware identity internally, so a rotated unique_id does not lift an active
ban. Unbanning with a stored ban_id continues to work after rotation, so
persist
the ban-id receipt to retain the ability to revoke a ban indefinitely. The
device's actual Android ID is never exposed to developers.
What You Should Store
The minimum fields you should persist to manage bans effectively and honor unban / detail flows.
| Field | Type | Required | Purpose |
|---|---|---|---|
unique_id |
string | Yes | Identifies the device for ban queries. |
ban_id |
string | Yes | Enables future unban and detail lookups. |
banned_at |
datetime | Optional | Audit trail for when the ban was issued. |
expires_at |
datetime | Optional | Known ban expiry for your own enforcement. |
| Reason / context | string | Optional | Which check or policy triggered the ban. |
Compliance Terms
The terms that govern ban enforcement, durations, expiry, and identifier rotation.
- Default duration: bans default to 7 days (168 hours).
- Maximum duration:
ban_time_in_hoursis capped at 1 year (365 days / 8760 hours). - Permanent bans: setting
ban_time_in_hoursto 0 creates a permanent ban that never expires. Permanent bans must be manually revoked by an administrator. - Automatic expiry: timed bans expire automatically; expired bans are
omitted from
list_bansand no longer enforced byverify_token. - Identifier rotation:
unique_idvalues rotate every 60 days. Active bans persist across rotation — Kubian enforces bans by hardware identity, not just theunique_id, so banned devices remain blocked even after rotation. - Masking:
ban-idvalues are masked to their first 5 hex characters in list responses (Administrative Isolation & Masking, Privacy Policy Section 3). - Single device assignment: a
unique_idis bound to your project's device list; bans cannot be placed on unknown identifiers.
device_ban_status or surface
DEVICE_AUTHORIZATION_FAILED
attestation failures, and gate gameplay accordingly.
Client API Overview & Usage
The Client API lets your game interact with player identities on Kubian:
log a device in, store profile data, and change profile names. It is designed for
in-game use through KubianCore, and every endpoint can be toggled on or off
per application from your dashboard.
Concepts
- Device accounts — the first time a verified device logs in,
Kubian automatically provisions an account (
Player-xxxxxxxx) and links it to that device. No signup flow is required. - Profile data — a small string key/value store attached to each
account (for example
rank,region,loadout). Values are visible in your dashboard under the player's Profile Data tab and are searchable by key/value. - Profile name — a human-friendly display name. It defaults to the auto-generated username until you or the player changes it.
- Sessions — login returns a session ticket used as the
X-Authorizationheader on subsequent calls.
Endpoints
| Method | Endpoint | In-game call | Purpose |
|---|---|---|---|
POST |
/client/create |
KubianCore.client.Create |
Create the device's account (auto-falls back to login). |
POST |
/client/login |
KubianCore.client.Login |
Log the device in and get a session ticket. |
POST |
/client/addProfileData |
KubianCore.client.addProfileData |
Merge key/value pairs into the account's profile data. |
POST |
/client/updateProfileName |
KubianCore.client.updateProfileName |
Change the account's profile name. |
In-game usage (KubianCore)
All of this is handled natively by the SDK once the platform is initialized:
// Device login (auto-provisions the account)
KubianCore.client.Login((userId, error) =>
{
if (error != null) { Debug.LogError(error); return; }
Debug.Log("Logged in as " + userId);
});
// Or restore a session created by your own trusted server:
KubianCore.client.loginCreds(sessionTicket, userId, accessToken, (uid, err) => { ... });
// Profile data
KubianCore.client.addProfileData("rank", "diamond", (resultJson, error) => { ... });
KubianCore.client.updateProfileName("ProKiller99", (resultJson, error) => { ... });
// Permanent project-scoped device id (see Permanent Account IDs)
string deviceId = KubianCore.device.getProperty.accountId();
// Sign an AXE account probe (see Account Verification)
KubianCore.device.transactions.accountVerification((probeToken, err) => { ... });
// Current session & stored profile data
string ticket = KubianCore.client.account.getProperty.sessionTicket();
string profileJson = KubianCore.client.account.getProperty.profileData();
The native layer builds the request bodies, attaches the one-time device certificate and hardware token, signs requests, and stores the session so profile APIs work immediately after login.
Client Login
POST /client/login exchanges a valid device certificate and
hardware token for an account session. Devices are authenticated exactly like integrity
verification: the certificate is one-time-use and consumed on success.
In-game call: KubianCore.client.Login
Request
{
"app_id": "your-app-id",
"android_id": "device-android-id",
"device_certificate": "<one-time certificate from init>",
"access_token": "HW|...|..."
}
Success response
{
"success": true,
"user_id": "...",
"unique_id": "abcd1234",
"username": "Player-4f7a21c9",
"profile_name": "Player-4f7a21c9",
"profile_data": { },
"last_login": null,
"session_ticket": "APPID.SESSIONID.ACTTOKEN.SIG",
"access_token": "HW|...|...",
"session_expires_in": 86400
}
The session ticket must be sent as the X-Authorization header on profile API
calls. Sessions expire after 24 hours.
Any time after signing in, the SDK can hand you the current state:
KubianCore.client.account.getProperty.sessionTicket() returns the active
session ticket and
KubianCore.client.account.getProperty.profileData() returns all stored profile
data as a JSON object string (refreshed on every login and after each profile update). Both
return null when not signed in.
Field meanings: username is the display name — it is what
/client/updateProfileName and /server/updateClientProfileName
change. profile_name is the stable profile identifier
(Player-xxxxxxxx) and never changes. Both start out equal on fresh accounts.
Banned device response
If the device's unique ID has an active ban, login is rejected with HTTP 403:
{
"success": false,
"error": "Unable to login to requested user account",
"message": "The requested account has been banned.",
"ban_details": {
"reason": "Cheating", // only if ban reason visibility is enabled
"duration": "23h 12m remaining", // only if duration visibility is enabled ("Permanent" for permanent bans)
"issued_at": "2026-08-22T10:15:00"
}
}
Reason and duration fields respect the dashboard's banning visibility settings;
issued_at is always present.
Errors
403— certificate invalid or already used; access token mismatch; Client login API disabled for the app.404— unknownapp_id.
Create an account
In-game call: KubianCore.client.Create
/client/create takes the exact same body as /client/login and
respects the same toggles and ban checks. It provisions the device's account and returns its
identity
without issuing a session:
{
"app_id": "your-app-id",
"android_id": "40628456",
"device_certificate": "-----BEGIN CERTIFICATE-----...",
"access_token": "HW|...|..."
}
{
"success": true,
"created": true,
"user_id": "...",
"unique_id": "abcd1234",
"username": "Player-8ebb770f",
"profile_name": "Player-8ebb770f"
}
If this device already owns an account, creation is rejected:
{
"success": false,
"error": "Account already exist",
"message": "This device is already connected to an account. Please log in instead."
}
You normally don't have to care: when KubianCore.client.Create receives the
Account already exist error, the native layer automatically falls back to logging
that account in — and after a fresh creation it signs in too. Either way your callback
fires with the logged-in userId:
KubianCore.client.Create((userId, err) =>
{
if (err != null) { Debug.LogError(err); return; }
// Signed in - account was created if missing, logged into otherwise.
});
Profile APIs
Both endpoints require a valid session from /client/login.
The session ticket travels in the X-Authorization header; the body carries the
hardware access token that identifies the caller.
Add profile data
In-game call: KubianCore.client.addProfileData
X-Authorization: APPID.SESSIONID.ACTTOKEN.SIG
{
"access_token": "HW|...|...",
"app_id": "your-app-id",
"profile_data": {
"rank": "diamond",
"region": "us-west"
}
}
Keys are merged into the existing profile data. Limits: 32 keys per account, keys up to 64 characters, values up to 512 characters.
{
"success": true,
"user_id": "...",
"added_keys": ["rank", "region"],
"profile_data": { "rank": "diamond", "region": "us-west" }
}
Update profile name
In-game call: KubianCore.client.updateProfileName
X-Authorization: APPID.SESSIONID.ACTTOKEN.SIG
{
"access_token": "HW|...|...",
"app_id": "your-app-id",
"username": "ProKiller99"
}
This sets the account's username — the display name shown in your
dashboard. The stable profile_name identifier (for example
Player-4f7a21c9) never changes. For compatibility, the legacy
profile_name body key is still accepted. Names must be 3–32 characters
and pass profanity filtering. The result appears in your dashboard's Players table as
<unique_id> - <username>.
Common errors
401— missing/invalid session ticket or access token.403— the corresponding Client API toggle is disabled for the app.400— validation failure (bad key/value/name).409— profile data limit reached.
Permanent Account IDs
Alongside per-device accounts, Kubian issues every physical device a permanent user_account_id: a 16-character numeric identifier derived from the device's Widevine ID and scoped to your application. It never expires and never rotates.
Properties
- Exactly 16 digits (0–9).
- Tied to the Widevine DRM ID of the device.
- Project-scoped — the same device receives a different account id in every application.
- Unchanging within your project: it survives reinstalls, factory resets, and cache clears.
- Issued lazily on first contact and stored permanently server-side.
Where you receive it
| Source | Field |
|---|---|
| Integrity init response | user_account_id |
| Challenge token payload | user_account_id |
| KubianCore (in-game) | KubianCore.device.getProperty.accountId() |
Usage
string permanentId = KubianCore.device.getProperty.accountId();
if (permanentId != null)
{
// Stable device identifier scoped to this application.
}
The value is available after platform initialization completes and is returned as
null when the device could not be identified.
accountId() returns null, no user_account_id is
included in integrity responses or challenge tokens, and
/server/verifyPermanentAccountId reports every id as unknown for your app.
Authentication & API Toggles
The Server-to-Kubian API lets your trusted backend manage player accounts directly — logging players in, writing profile data, and renaming profiles — without a device present.
Authentication
Every request must include your application credentials in the access-token
header:
access-token: KB|<app-id>|<app-secret>
You can find both values in your dashboard under application credentials. Requests with
invalid credentials are rejected with 401 Invalid app credentials.
Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/server/create |
Create an account using your own user_id (max 32 characters). |
POST |
/server/login |
Log an existing account in by user_id. |
POST |
/server/addClientProfileData |
Merge profile data into any of your accounts. |
POST |
/server/updateClientProfileName |
Rename any of your accounts. |
POST |
/server/verifyPermanentAccountId |
Verify a device's permanent account id. |
POST |
/verify/transaction/probe_transaction_token |
Verify an AXE account probe token and learn its account id. |
API toggles
The dashboard's API Settings → Client APIs panel controls three switches that apply to both the client endpoints and their server equivalents:
- Client login — gates
/client/login,/client/createand/server/login. - Update profile name — gates
/client/updateProfileNameand its server variant. - Add profile data — gates
/client/addProfileDataand its server variant.
When a toggle is disabled, affected endpoints return:
{ "success": false, "error": "... disabled for this application." }
/server/create & /server/login
Create accounts from your backend, then log them in by user_id. Creation and login are separate calls: create once when your player signs up on your platform, then log in whenever they play.
Create an account
You supply the user_id — typically your own platform's stable player
id, up to 32 characters. Kubian uses it as the account's
permanent identifier, so /server/login then works with the exact same
value.
access-token: KB|your-app-id|your-app-secret
{
"user_id": "7656119800000000",
"username": "CoolPlayer" // optional display name
}
{
"success": true,
"user_id": "7656119800000000",
"username": "CoolPlayer",
"profile_name": "Player-4f7a21c9"
}
If the user_id is already taken by any account, creation is rejected:
{
"success": false,
"error": "Unable to use this user_id.",
"message": "The attempted use user_id is already connected to an account."
}
Log an account in
access-token: KB|your-app-id|your-app-secret
{
"user_id": "7656119800000000"
}
Login never creates accounts. If no account exists for the supplied
user_id (or it belongs to a different application), you get:
{
"success": false,
"error": "No account was found to login to with this user_id",
"message": "Account not found. Please make sure the specified user_id is actually apart of an account."
}
Response
{
"success": true,
"user_id": "...",
"unique_id": "abcd1234",
"username": "Player-9c21ba07",
"profile_name": "Player-9c21ba07",
"profile_data": { },
"last_login": "2026-08-21T18:04:11",
"session_ticket": "APPID.SESSIONID.ACTTOKEN.SIG",
"access_token": "ACT|...",
"session_expires_in": 86400
}
Handing the session to the game client
Deliver session_ticket, user_id and access_token to
your game over your own secure channel, then install them natively:
KubianCore.client.loginCreds(sessionTicket, userId, accessToken, (uid, err) =>
{
if (err != null) { Debug.LogError(err); return; }
// Profile APIs now work without a device-side login.
});
Bans
If any device linked to the account carries an active ban, login is rejected with the same banned-account payload documented on the Client Login page.
Verify a permanent account id
/server/verifyPermanentAccountId checks whether a permanent account id is
valid for your application. Clients get this id in-game from
KubianCore.device.getProperty.accountId() — a 16-digit number
tied to the device's Widevine ID, scoped to your project (see Permanent Account IDs). If your
project has no approved Account ID Collection request, every lookup returns
valid: false:
access-token: KB|your-app-id|your-app-secret
{
"user_account_id": "4062845600424304"
}
A known id returns valid: true together with its creation date:
{
"success": true,
"valid": true,
"user_account_id": "4062845600424304",
"created_at": "2026-08-01T12:00:00"
}
An unknown id is still a successful verification — it simply doesn't exist in your application:
{
"success": true,
"valid": false,
"user_account_id": "4062845600424304",
"message": "No permanent account exists for this user_account_id in this application."
}
Ids that aren't exactly 16 digits are rejected with 400.
Server Profile APIs
Both endpoints target any account belonging to your application. They respect the same validation rules and limits as their client-side counterparts.
Add profile data
access-token: KB|your-app-id|your-app-secret
{
"user_id": "...",
"profile_data": {
"rank": "diamond",
"matches_played": "142"
}
}
{
"success": true,
"user_id": "...",
"added_keys": ["matches_played", "rank"],
"profile_data": { "rank": "diamond", "matches_played": "142" }
}
Update profile name
access-token: KB|your-app-id|your-app-secret
{
"user_id": "...",
"username": "RenamedByServer"
}
Sets the account's username (display name). The stable
profile_name identifier never changes. The legacy
profile_name body key is still accepted.
Errors
400— missing identifier, oversized key/value, invalid name.401— badKB|credentials.403— matching Client API toggle disabled.404— unknownuser_id.409— profile data limit reached.
ACT — Account Content Token
The credential that keeps you signed in between visits, without asking you to re-enter your password (and 2FA code, if enabled) every time.
What is ACT?
ACT stands for Account Content Token. It is the credential Kubian issues to an account after a successful sign-in with your password — plus your 2FA code, if you have two-factor enabled.
Think of it like a hotel key card: once you have checked in, you get a card that lets you back into your room without going to the front desk again. The card only works for your room, it expires after a set period, and if you ever change your password, every card that was ever issued is cancelled.
An ACT is tied to your account, not to any single device. Every device you sign into the same account with uses the same ACT — so seeing the same ACT on all of your browsers is normal and expected. The separate TAM check (next page) is what proves a request is actually coming from one of your trusted devices.
Elements of an ACT
- Issued post-authentication — only after your password (and 2FA code, if enabled) has been validated.
- Account-bound — it cannot be reused on another account, and it cannot be reverse-engineered to reveal your password.
- Expiring — once the built-in window passes, the token stops working on its own and you are simply asked to sign in again.
- Revocable — changing your password invalidates every previously issued ACT immediately.
- Not self-sufficient — on any device with a registered key, a valid ACT alone is not enough; a fresh TAM device signature is required alongside it.
Kubian intentionally does not publish the internal mechanics of how ACTs are constructed or verified — that is part of what keeps them safe from being forged or tampered with.
TAM & CAT
The Trusted Account Module ties your account to the specific device you sign in from, and the Challenge Account Token is the short-lived proof that the device still holds its key.
What is TAM?
TAM stands for Trusted Account Module. A key card can be copied — and so can an ACT if it were ever intercepted (through a compromised browser extension, a malicious script, or a leaked log). TAM closes that gap.
When you first sign in on a device, TAM has that device generate a cryptographic key pair of its own. The private half is created in a way that makes it non-exportable: your browser can use it to sign — proving "this is the same device that registered" — but nothing, not a script running on the page, not a browser extension, not even you looking through developer tools, can ever read the key's raw contents out of the browser. Only the matching public half is sent to us and stored on your account.
From that point on, signing in requires two things together: a valid ACT and proof from that specific device's TAM key. Stealing the ACT alone is no longer sufficient.
- TAM keys are stored per device and each one is valid for 14 days. When a key expires, the device is asked to sign in again with your password, and a fresh key is issued.
- If a new device is ever added to your account, we send a confirmation email to your account's address before that device is trusted.
What is CAT?
CAT stands for Challenge Account Token — the short-lived challenge the Trusted Account Module issues when a device needs to prove it still holds the key it registered.
Each CAT is generated fresh by us, is valid for only 120 seconds, and is cryptographically sealed so it cannot be forged, altered, or reused for a different account. Your browser never invents it — it only ever comes from us. To complete a sign-in, the device signs the CAT with its private TAM key and sends back only the signature. Because a CAT only works for the brief window it takes to verify a signature, a stolen or intercepted CAT on its own is worthless.
How TAM verifies a sign-in
- 1. Token check — your browser presents its ACT; we confirm it is genuine and has not expired.
- 2. CAT issued — we generate a short-lived CAT, valid for 120 seconds, and send it to your browser.
- 3. Device proof — your browser signs that CAT with its private TAM key and sends back only the signature.
- 4. Session granted — we verify both the CAT's authenticity and its signature against the TAM public key on file for your device. Only then is your session actually created. If the signature is missing, does not match, or the CAT has expired, sign-in is refused — even with a valid ACT.
The CAT is sealed with a server-held key using authenticated encryption, so it cannot be forged, decoded, or edited by anyone without that key, and it stops working entirely once its 120-second window closes.
How Your Account Is Protected
What actually has to be true for a sign-in to be accepted — and what happens if a token is stolen, expires, or is invalidated.
The bar for a valid session
Only a token that was legitimately issued to your account, that has not expired, that has not been invalidated by a password change, and that is backed by a valid TAM device signature will ever work. All four conditions must hold at once.
- Legitimately issued — generated only after you signed in with your password (and 2FA code, if enabled).
- Not expired — an ACT carries a built-in expiration; once the window passes it stops working on its own.
- Not invalidated — changing your password invalidates every previously issued token, so a token from before the change can no longer be used.
- Device-backed — on a device with a registered key, a fresh TAM signature is required alongside the ACT.
Combined, these mean both your identity and your trusted device must be confirmed before a session is created.
Instant re-login
"Instant re-login" is the everyday benefit of holding a valid ACT plus a registered device key: when you come back to an app or site, your device presents both and you are back in immediately — no typing, no waiting on a fresh login screen.
- Fast — your session resumes right away instead of requiring you to sign in again.
- Seamless — there is no interruption between visits on any of your trusted devices.
- Safe — the same device-key requirement that makes re-login instant is what makes a stolen ACT useless on its own.
If a token has expired, is malformed, or is missing its device signature, re-login is not granted; you simply go back through sign-in.
Password and token invalidation
Changing your password invalidates every previously issued token the moment the new password takes effect. Every device using a stored sign-in must sign in again with your password (and 2FA code, if enabled) before it can continue. This is the fastest way to lock out anyone who might hold an old sign-in.
2FA, Trusted Devices & Recovery
Two-factor authentication, managing the devices trusted on your account, and what to do if you ever need to lock everything down.
Two-factor authentication (2FA)
Enabling 2FA adds a second check at sign-in, so your password alone is not enough to reach your account. With 2FA on, even a compromised password is not sufficient on its own to sign in.
- TOTP authenticator — a 6-digit code from your authenticator app, generated from a per-account secret configured during setup.
- Applied at the gate — the 2FA code is required at sign-in, at password reset, and on account-recovery paths before anything changes.
- Preserved on lock — if your account is ever locked for protection, your existing 2FA settings are preserved so you can unlock with them.
Setup is done through a secure setup flow that displays a QR code (or a manual secret) and requires you to confirm a generated code before 2FA is enabled.
Managing trusted devices
The Account Administration page (under Account Settings) gives you a live view of every trusted device signed into your account. For each device you can see its sign-in status and when it was last used.
- Sign out one device — removes that device's TAM key from your account. The device can no longer use its stored sign-in; it will be asked for your password (and 2FA code, if enabled) before it can access your account again.
- Sign out all devices — removes every TAM key on the account at once. Every device, including the one you are using now, must sign in again with your password and 2FA before it can continue.
A confirmation email is sent to your account's security address when a device is signed out. If a sign-out wasn't yours, the email lets you secure your account immediately.
Account lock & recovery
If a sign-in attempt looks like it could be an attacker, Kubian may lock the account: all trusted device keys are invalidated and a recovery email is sent with instructions to reset your password, re-enable/confirm two-factor authentication, and unlock the account.
- Reset your password — choose a new, strong password. Every previously issued token is invalidated the moment the new password takes effect.
- Set up 2FA — configure a fresh authenticator (TOTP) key or confirm email verification before unlocking, so the account cannot be reached with a password alone.
- Unlock and re-register — unlock the account manually with your new password, then re-register your trusted devices.
Recommended account hygiene
- Use a strong, unique password — your ACT is only as safe as the password that protects it; no server-side security makes up for a weak, reused, or shared password.
- Enable 2FA — the single strongest step you can take; with 2FA on, even a compromised password is not enough on its own to sign in.
- Sign out devices you don't use — remove a trusted-device key entirely from a machine you've stopped using; that device will need your password (and 2FA) before it can access your account again.
- Change your password if anything looks unfamiliar — doing so immediately revokes every stored sign-in everywhere.
Account Verification Flow
Account verification proves that a device owns a permanent, project-scoped user_account_id — with one call on the device and one call on your backend. The device receives an AXE-encrypted probe token it cannot read or forge; your server verifies that token and learns the account id.
- Device signs a probe. Your game calls
KubianCore.device.transactions.accountVerification(). The SDK builds and sends a signed request; the server verifies the device, resolves the project-scoped account id, and returns an AXE-encryptedprobe_transaction_token(valid 10 minutes, single-use). - Device hands the token to your backend over your own secure channel (for example alongside your login request). The token is opaque to the client.
- Your server verifies the probe with
POST /verify/transaction/probe_transaction_tokenusing yourKB|credentials. Kubian opens the AXE envelope, checks authenticity, expiry and single-use, verifies the bound nonce, and returns theuser_account_id. - Your server confirms the id with
POST /server/verifyPermanentAccountId— the same check used for ids read in-game viaKubianCore.device.getProperty.accountId().
In-game usage (KubianCore)
KubianCore.device.transactions.accountVerification((probeToken, error) =>
{
if (error != null) { Debug.LogError(error); return; }
// Send probeToken to YOUR backend - never verify it on-device.
SendToGameServer(probeToken);
});
- Widevine identity — the probe requires the Widevine ID (plus security level), which is the stable account identity that survives factory resets. The SDK supplies both automatically.
- Access is gated — like every account-id surface, probing
requires an approved Account ID Collection request for your project
(dashboard → Data Collection Request tab). Without it, signing returns
403.
Verify Probe Token
POST /verify/transaction/probe_transaction_token verifies
the authenticity of an account probe on your trusted backend and returns the
project-scoped hardware id — the same value
KubianCore.device.getProperty.accountId() reports in-game.
Request
access-token: KB|your-app-id|your-app-secret
{
"probe_transaction_token": "AXE...."
}
Authentication is your KB|app-id|app-secret server credential — the same
header used across the Server API. The body also accepts probe_token or
token as the key.
What verification checks
- The AXE envelope opens with the server key (authenticity — forged or edited tokens fail).
- The probe belongs to your application and has not expired.
- The token has not been used before (single-use
jti). - The nonce sealed inside is pulled out and verified: known, consumed by signing, and bound to the same probe and account.
Success response
{
"success": true,
"valid": true,
"user_account_id": "4062845600424304",
"app_id": "your-app-id",
"created_at": "2026-08-01T12:00:00"
}
An unknown id is still a successful verification — it simply doesn't exist in your
application (valid: false with the same shape).
valid: true response already confirms the id for your application —
there is nothing further to prove. If you want to re-check the same id later (for
example on subsequent sessions, without spending a fresh probe),
POST /server/verifyPermanentAccountId accepts the bare id any time.
Direct link to this page:
https://kubian.app/integrity/docs#account-verify-page-4
Errors
400— missing body, missing token, or not an AXE token.401— badKB|credentials, forged/expired/replayed token, or any nonce check failure.403— token belongs to a different application.
Every exact error string is listed on the next page: Success & Failure Responses.
Success & Failure Responses
The complete response reference for
POST /verify/transaction/probe_transaction_token — what each field
means and exactly what failure looks like.
Success — verified
| Field | Description |
|---|---|
success |
Request processed. Always true here. |
valid |
true — the probe is authentic and the account id exists in
your application. Conclusive: no further check needed. |
user_account_id |
The 16-digit project-scoped id. Same value as
KubianCore.device.getProperty.accountId(). |
app_id |
Echo of your application id. |
created_at |
When the permanent account record was created. |
{
"success": true,
"valid": true,
"user_account_id": "4062845600424304",
"app_id": "your-app-id",
"created_at": "2026-08-01T12:00:00"
}
Success — unknown id
A well-formed, authentic probe for an id that does not exist in your application is still
a successful verification — it simply reports valid: false:
{
"success": true,
"valid": false,
"user_account_id": "4062845600424304",
"message": "No permanent account exists for this user_account_id in this application."
}
Failure responses
Every failure returns success: false, valid: false with an
error string. Verification consumes the token, so a failed attempt cannot
be retried with the same token — request a fresh probe.
400 Bad Request — the request itself is unusable
| Error | Meaning |
|---|---|
Missing JSON body. |
No JSON body was sent at all. |
Provide the 'probe_transaction_token' to verify. |
Body present but no token under probe_transaction_token,
probe_token or token. |
Invalid probe_transaction_token format. |
Token does not start with AXE. — not a probe token. |
{
"success": false,
"valid": false,
"error": "Provide the 'probe_transaction_token' to verify."
}
401 Unauthorized — credentials or token rejected
| Error | Meaning |
|---|---|
Missing or invalid access-token header. Expected format:
KB|app-id|app-secret |
No access-token header, or it does not start with
KB|. |
Malformed access-token header. Expected format:
KB|app-id|app-secret |
Header is not exactly KB|app-id|app-secret. |
Invalid app credentials. |
App id/secret pair does not match any project. |
Invalid probe_transaction_token. |
AXE envelope failed to open (forged, corrupt, or wrong key), wrong payload type, missing replay id, or missing nonce/account inside. |
This probe_transaction_token has expired. |
Past its 10-minute window — request a fresh probe. |
This probe_transaction_token has already been used. |
Replay: single-use tokens are consumed on first verification, success or failure. |
Invalid user_account_id in probe. |
Sealed id is not 16 digits. |
Probe nonce is unknown. |
Sealed nonce was never issued for your application. |
Probe nonce was never signed. |
Nonce exists but was never consumed by signing. |
Probe nonce mismatch. |
Nonce is bound to a different probe than this token. |
Probe account mismatch. |
Nonce is bound to a different account id than the sealed one. |
{
"success": false,
"valid": false,
"error": "This probe_transaction_token has already been used."
}
403 Forbidden — right token, wrong application
{
"success": false,
"valid": false,
"error": "This probe_transaction_token does not belong to this application."
}
What is Kubian Integrity AC
Kubian Integrity answers "can this device be trusted?" at session start. Kubian Integrity AC answers "is it still trustworthy?" — every two seconds, for the entire session. It is the always-on runtime defense layer: Real-Time Environment Protection (RTEP), device banning, and per-check enforcement, running natively inside your app.
Three native libraries, one shield
AC ships as compiled .so libraries with plain C entry points — no
Java attack surface, symbols hidden, stack-protector hardened. Each library owns one
job:
libKubianIntegrityCore.so
The protection loop itself: StartProtection,
ScanThreats, threat sweeps, realtime alerts, ban polling, tier
gating, and session APIs.
libKubianIntegrity.so
The two-step challenge exchange: fetches a single-use challenge, mints a fresh hardware-backed key bound to its nonce, and trades the attestation chain for an integrity token.
libKubianIntegrityCertificate.so
One-time device certificates from KeyStore attestation, with key material wiped from memory after use and keys rotated ephemerally.
Integrity vs AC at a glance
| Aspect | Kubian Integrity | Kubian Integrity AC |
|---|---|---|
| Question answered | Can this device be trusted right now? | Is it still trustworthy, right now? |
| When it runs | Once, at session start (point-in-time proof) | Continuously, every 2 seconds for the whole session |
| Where it runs | Server-side verification of presented proof | Natively inside your app process + server-directed enforcement |
| Core mechanism | Hardware attestation, X.509 chains, KeyMint data | RTEP sweeps, realtime alerts, device bans |
| Outcome | Integrity token: admit or refuse the session | Alert, Kick, or Ban — per check, per your policy |
How RTEP Works
Real-Time Environment Protection is a native protection loop with a server-side brain. The client sweeps for threats every 2 seconds; the server decides what each detection means — Alert, Kick, or Ban — and the client enforces it. Here is the full loop.
- Start protection. Your integration calls
StartProtectionwith a sweep interval (minimum 500 ms, typically 2000 ms) and an enforce flag. A detached monitor thread spins up — gameplay never waits on it. - Sweep in slices. Each cycle runs the hard and soft scan sets, sleeping in 100 ms slices between work so the loop stays responsive and never hitches the game thread.
- Detect the change. Results are compared against the previous scan. On any change, the detection callback fires with full detail and a realtime alert is reported to the backend — app, device identity, device certificate, check, and detail.
- Server directs. The backend evaluates the studio's configured action
for that check and answers with a directive:
note(log only),kick(remove from session), or an executed device ban. In enforce mode, Kick and Ban terminate the process immediately. - Re-verify forever. Ban status is re-polled every 10 seconds, the protection tier is re-fetched every 60 seconds, and device certificates renew themselves before expiry — the loop never goes stale.
One-shot scans
ScanThreats runs the same hard + soft scan set exactly once — same tier
gating, same reporting — for integrations that want a point-in-time verdict (for
example, right before a ranked match starts) without starting the continuous loop.
Ban polling
Every 10 seconds the monitor asks the server for the device's current ban state. If a ban
lands mid-session, the player is kicked immediately and the server records a
REALTIME_BANNED_WHILE_PLAYING audit event. Devices banned before protection
started are not re-kicked — your login-time attestation path owns those.
Tier recheck
Every 60 seconds the library re-fetches the protection tier. If the project drops below Max, realtime checks stand down and only the ban poll continues. The tier response is RSA-signed: any unsigned or tampered reply is rejected and the client fails closed into full Max-tier protection.
Defenses Catalog
Every check RTEP runs, what it catches, and what happens by default. Checks and actions are configurable per project in the Kubian dashboard (Max tier) — this table is the starting policy. Per-check internals live in the Real-time Protection Reference.
| Check | Catches | Default |
|---|---|---|
frida |
Frida instrumentation framework | Alert |
injection |
Code / library injection into the process | Alert |
root_hard |
Persistent / privileged root (e.g. Magisk full install) | Ban |
root_soft |
Soft or hidden root indicators, emulator environment | Kick |
debugger |
Attached debuggers (ptrace, JDWP, LLDB) | Kick |
antidebug |
Anti-debug bypass attempts | Alert |
headlock_abuse |
Quest headlock hijacking for spatial bypass | Kick |
frida_behavioral |
Anonymous executable memory (patched Frida builds) | Kick |
frida_module |
Renamed / patched Frida binaries | Kick |
frida_memfd |
In-memory Frida injection via fd leaks | Kick |
widevine_hook |
MediaDrm hooking, invalid / tampered Widevine IDs | Ban |