Skip to main content

Client Endpoints

The /api/client/* routes are the four calls a licensed application makes at runtime: open a session with init, then hit register, login, or license depending on how you authenticate the user.

Every request is POST with a JSON body and every response is JSON. Every call carries the HMAC headers from Request Signing, and every call after init carries the session token that init returned. Base URL and shared transport rules live in API Overview.

POST /api/client/init

Opens a session. Every other client call needs the token this returns.

Before endpoint logic runs, the server enforces the per-IP rate limit, verifies the signature, screens the IP against the app blacklist, and refuses if the application is disabled. When force_update is set on the app and the client version does not match the server version, the request is rejected with a 426.

Request body

FieldTypeRequiredNotes
api_keystringyesPublic application key.
versionstringno, default "1.0"Client-reported version, compared to app version.
enc_keystring, max 35noClient-side encryption key, required when force_encryption is on.
init_ivstring, max 64noPer-request IV seed for AES-256-CBC response encryption.
binary_hashstring, max 128noSHA-256 or MD5 of the client binary, checked when hash_check is on.
binary_tokenstring, max 128noPre-issued binary token embedded in the client build.
binary_token_hashstring, max 128noHMAC-SHA256(binary_token, api_secret) proving token origin.

Success response

{
"success": true,
"session": "<opaque session token>",
"app": {
"name": "My App",
"version": "1.0",
"hwid_lock": true
}
}

Errors

StatusDetailCause
403disabled_message on the app, or "Application disabled"App is not enabled.
426{"message", "latest_version", "update_url", "force_update": true}Version mismatch while force_update is on.
429Rate limitMore than 30 inits per minute from one IP.
Version mismatch without force_update

The call still succeeds. The server writes a client.version_mismatch audit entry at warning level and proceeds. Only force_update turns the mismatch into a hard refusal.

POST /api/client/register

Claims a license key and creates a new end user in one transaction. On success the server calls mark_session_activated for the username, matching the login flow so the heartbeat session binds to the new account immediately.

The new user row is created with last_login set to null. Only a subsequent successful login stamps it.

Request body

FieldTypeRequiredNotes
sessionstringyesFrom init.
usernamestringyesMust be unique inside the app.
passwordstringyesMax 72 UTF-8 bytes (bcrypt limit).
licensestringyesUnused license key for this app.
hwidstring, 8-256 charsyesNormalised to lowercase server-side.
screenshotstringnoBase64 screenshot for the activation capture.
init_ivstring, max 64noResponse-encryption IV.

Success response

{
"success": true,
"message": "Account created",
"user": {
"username": "alice",
"level": 1,
"expires_at": "2026-10-03T12:00:00+00:00"
}
}

Errors

StatusDetailCause
400"Invalid license key"Key does not exist for this app.
400"License key already used"Key status is used, or was claimed by another activation between fetch and update.
400"Username already taken"Username exists for this app, or a duplicate insert raced.
403"License key is banned"Key status is banned.
403"Application disabled" or the app's disabled_messageApp disabled.
403"License validation is temporarily paused for this application."keys_paused on the app.
409"License state changed mid-activation, please retry"Key moved out of pending between claim and finalise.
429"Too many failed attempts, try again later."Register lockout.
429Rate limitMore than 20 registers per minute from one IP.

License state during register moves unused -> pending -> used. If any exception fires while the row is pending, the server resets it to unused and re-raises, so a crashed activation never leaves a key wedged.

POST /api/client/login

Authenticates an existing end user. The response fails uniformly with "Invalid username or password" for a wrong password, a banned user, an expired user, or an HWID mismatch, so an attacker cannot tell which of those tripped.

Request body

FieldTypeRequiredNotes
sessionstringyesFrom init.
usernamestringyes
passwordstringyesMax 72 UTF-8 bytes.
hwidstring, 8-256 charsyesNormalised to lowercase.
screenshotstringnoBase64 screenshot for the login capture.
init_ivstring, max 64noResponse-encryption IV.

Success response

{
"success": true,
"message": "Authenticated",
"user": {
"username": "alice",
"level": 1,
"expires_at": "2026-10-03T12:00:00+00:00"
}
}

Errors

StatusDetailCause
401"Invalid username or password"Wrong username, wrong password, banned user, expired user, or HWID mismatch when hwid_lock is on.
403"Application disabled" or the app's disabled_messageApp disabled.
403"Access is temporarily paused for this application."keys_paused on the app.
403"Heartbeat session required. Call /heartbeat/start first."require_heartbeat is on and the session has no live heartbeat. See Heartbeat.
429"Too many failed attempts, try again later."Login lockout.
429Rate limitMore than 20 logins per minute from one IP.
HWID pinning on first login

When hwid_lock is on and the user has no stored HWID yet, the first successful login writes the provided HWID to the user record. Every login after that must match it.

POST /api/client/license

Validates a license key without a username or password. Useful for license-only apps that do not maintain end-user accounts.

First use of an unused key promotes it to used, stamps expires_at, sets used_by to "license-only", and pins hwid to the caller.

Request body

FieldTypeRequiredNotes
sessionstringyesFrom init.
licensestringyesLicense key for this app.
hwidstring, 8-256 charsyesNormalised to lowercase.
screenshotstringnoBase64 screenshot for the activation capture.
init_ivstring, max 64noResponse-encryption IV.

Success response

{
"success": true,
"message": "License valid",
"level": 1,
"expires_at": "2026-10-03T12:00:00+00:00"
}

Errors

StatusDetailCause
400"Invalid license key"Key does not exist for this app.
403"Application disabled" or the app's disabled_messageApp disabled.
403"License validation is temporarily paused for this application."keys_paused on the app.
403"Heartbeat session required. Call /heartbeat/start first."require_heartbeat is on and the session has no live heartbeat.
403"License key is banned"Key status is banned.
403"License not valid"Key status is not one of unused or used (after the banned and pending checks).
403"HWID mismatch. Key locked to another device."hwid_lock is on and the key's stored HWID differs.
403"License expired"Stored expires_at is in the past.
409"License activation in progress, retry."Key is pending on a concurrent register.
429Rate limitMore than 20 license calls per minute from one IP.

Shared behaviour

Every client endpoint runs the same gate before the endpoint-specific logic:

  1. Per-IP rate limit (30/min for init, 20/min for the others).
  2. HMAC signature check (see Request Signing).
  3. Blacklist check on IP and, where present, HWID.
  4. enabled check on the application.

Success and failure both write to the app's audit log. Register, login, and license also fire an activation webhook when the app has one configured, including geo lookup and the optional screenshot capture.