API reference
Every HTTP endpoint on the SDKey Worker, including client auth and app settings.
Conventions
- Base URL:
https://api.sdkey.dev - Base path:
/api/v1(plusGET /health) - Dashboard / tooling auth: cookie
sdkey_session(EdDSA JWT, 7 days) orAuthorization: Bearer sdk_live_… - Client license path and end-user auth: no developer auth — see License control
- Rate limits are per IP per 60s window unless noted
Health
Auth: Public
{ "ok": true, "service": "sdkey-api" }Auth (developer)
Auth: Public (rate: 20/min)
Body: email, password (12–128), name, turnstileToken (Cloudflare Turnstile). Sets session cookie. 201 { success, developer }.
Auth: Public (rate: 20/min)
Body: email, password, turnstileToken. Sets session cookie.
Auth: Public
Clears session cookie.
Auth: Cookie or Bearer
{ success, developer }
Apps
All routes require cookie or Bearer.
Auth: Required
Body: name, optional version (default 1.0.0). Returns application + sdkHint; embed publicKeyB64 in clients. Defaults: hwidLockEnabled: true, requireLicenseToRegister: true, resolved responseMessages.
Auth: Required
{ success, applications }
Auth: Required
Single application owned by the developer (includes settings fields and resolved message map).
Auth: Required
Permanently delete the application. Cascades licenses, end users, bans, and security logs. Returns { success: true }. Missing or non-owned app → 404 APP_NOT_FOUND.
Auth: Required
Partial update. At least one field required. Body (all optional):
{
"version": "1.0.1",
"status": "active" | "disabled",
"hwidLockEnabled": true,
"requireLicenseToRegister": true,
"responseMessages": {
"OK": "validated",
"APP_OUTDATED": "Client version outdated",
"HWID_BANNED": "Hardware ID is banned",
"IP_BANNED": "IP address is banned",
"LICENSE_REQUIRED": "A valid license key is required to register",
"TIER_NOT_HIGHER": "License tier must be higher than the current tier"
}
}Editable message keys: APP_DISABLED, OK, BANNED, HWID_MISMATCH, LICENSE_NOT_FOUND, EXPIRED, APP_OUTDATED, HWID_BANNED, IP_BANNED, REGISTER_DISABLED, LICENSE_REQUIRED, INVALID_CREDENTIALS, USERNAME_TAKEN, USER_NOT_FOUND, TIER_NOT_HIGHER. Sent keys merge over stored overrides; response returns the full resolved map. { success, application }.
Where the text appears in JSON: sealed validate / sealed client auth — inner plaintext message (success uses OK where applicable); plaintext validate — top-level message on success, error on failure; session init and plaintext client-auth failures (CRYPTO_ENFORCE=false only) — error. Session init success bodies have no customizable message field.
Auth: Required
List HWID/IP bans for the app: { success, bans } — each ban has id, banType (hwid | ip), value, sourceLicenseId, createdAt.
Auth: Required
Remove a ban row. Does not unban the license status — use only to clear HWID/IP ban entries. { success: true }.
Auth: Required
Query: limit, cursor, optional q (substring search). App end-users (username, email, licenseId, subscriptionTier, lastIp, lastHwid). subscriptionTier is null when unlinked; otherwise an integer ≥ 0. { success, users, nextCursor }.
Auth: Required
Create an app end-user from the dashboard or tooling API. Does not issue a client session. Body: username (3–64, [a-zA-Z0-9._-]), password (min 8), optional email, optional licenseKey. If the app has requireLicenseToRegister, licenseKey is required (400 LICENSE_REQUIRED) — same rule as client register; no admin bypass. When a key is provided, unused licenses are activated without binding HWID. Logs user_created. Errors: 409 USERNAME_TAKEN, 404 LICENSE_NOT_FOUND, 403 BANNED / EXPIRED / APP_MISMATCH. { success, user } (201).
{
"username": "alice",
"password": "securepass",
"email": "[email protected]",
"licenseKey": "XXXX-...."
}Auth: Required
Full end-user export for data-subject / portability workflows (controller assistance). { success, export }. See Data protection.
Auth: Required
Erase an app_users row. Does not revoke the linked license or purge historical security logs. Logs user_deleted.
Auth: Required
Clear the user's lastHwid. If the user has a linked license, also clears that license's device binding (same as POST /licenses/:id/reset-hwid) without changing status or expiry. Next validate/login can bind a new HWID. Logs hwid_reset. Errors: 400 HWID_NOT_SET when nothing to clear; 400 LICENSE_BANNED if the linked license is banned.
Auth: Required
Query: limit, cursor, optional q. Paginated license list (includes subscriptionTier, lastIp).
Auth: Required
Body: seconds (positive, max ~10 years). Extends active licenses for the app.
Auth: Required
Query: limit, cursor, optional q. Security / validation logs.
Licenses (developer)
Auth: Required (rate: 30/min)
Default account mode — server mints plaintext keys once.
{
"applicationId": "<uuid>",
"amount": 1,
"durationSeconds": 2592000,
"note": "optional",
"mask": "SDKY-XXXX-XXXX-XXXX-XXXX",
"subscriptionTier": 0
}subscriptionTier is an integer ≥ 0 (default 0). 201: { success, keys, warning } — each key includes subscriptionTier. Server stores SHA-256 + prefix.
Auth: Required (rate: 30/min)
Private mode — client mints; send hashes only (no plaintext licenseKey).
{
"applicationId": "<uuid>",
"subscriptionTier": 0,
"keys": [
{
"licenseKeyHash": "<64 hex sha256>",
"licenseKeyPrefix": "SDKY-ABCD…",
"durationSeconds": 0,
"noteCiphertextB64": "optional",
"subscriptionTier": 1
}
],
"mask": "optional"
}Batch subscriptionTier is the default when a key omits its own tier.
Auth: Required
Ban a license; invalidates KV cache. Optional body:
{
"scope": "license" | "hwid" | "hwid_and_ip",
"ip": "optional override for hwid_and_ip"
}license(default) — set license status to bannedhwid— ban license + insert HWID ban (requires bound HWID)hwid_and_ip— ban license + HWID + IP (IP from body orlastIp)
Auth: Required
Also PATCH. Set any integer tier ≥ 0: { "subscriptionTier": 2 } → { success, subscriptionTier }. Invalidates KV cache.
Auth: Required
Clear the license device binding (hwid → null). Keeps status, expiry, and activation time so remaining duration is preserved. Invalidates KV cache. Logs hwid_reset with the previous HWID. Next validate (or client login with HWID lock on) rebinds to the new device without restarting the timer. Errors: 400 HWID_NOT_SET, 400 LICENSE_BANNED, 404 LICENSE_NOT_FOUND. Does not remove application_bans HWID rows — use ban delete for that.
Auth: Required
Delete a license.
Session & validate (client)
Public SDK endpoints — detailed guide: License control.
Auth: Public (rate: 60/min)
Body: appId, clientNonceB64 (32 bytes), clientVersion (exact match against app version). IP ban checked. Failures: APP_DISABLED, APP_OUTDATED, IP_BANNED — plaintext { success: false, error, code } (error is the editable message). Success returns signed hello + session material with no message field.
Auth: Public (rate: 120/min)
Sealed AES-GCM envelope required in production. Inner hwid is optional — when omitted, HWID lock / mismatch / HWID-ban are skipped. When present and hwidLockEnabled, first success binds HWID. After decrypt, success and failure plaintext both include message (success uses the OK response message) plus integer subscriptionTier on success:
{
"success": true,
"code": "OK",
"message": "validated",
"status": "active",
"expiresAt": "…",
"subscriptionTier": 0,
"sessionId": "…",
"timestamp": 1720000001,
"v": 1
}Plaintext validate (tests / when crypto not enforced): success has top-level message; failures use error. See License control.
Client auth (end-user)
Sealed session (same outer envelope as validate). Rate: 30 / min / IP. Call POST /session/init first; applicationId and version come from the session (no appId / clientVersion in the inner body). Subject to IP/HWID bans. Response is sealed + signed; after decrypt, both success and failure include message. Success also carries sessionToken, expiresAt, user, license, session — license may be null; never includes password. Opaque session TTL: 7 days. When CRYPTO_ENFORCE=true, plaintext bodies → 400 CRYPTO_REQUIRED.
Auth: Public sealed (rate: 30/min)
Outer envelope: { sessionId, ivB64, ciphertextB64, tagB64 }.
{
"username": "player1",
"password": "••••••••",
"email": "[email protected]",
"licenseKey": "SDKY-....",
"hwid": "optional",
"nonce": "<base64 ~16 bytes>",
"timestamp": 1720000001,
"v": 1
}Username: 3–64, [a-zA-Z0-9._-]. Password: 8–128. If requireLicenseToRegister is true (default), licenseKey is required (LICENSE_REQUIRED). Mandatory client order: open → Ed25519 verify → sessionId + skew → honor success.
Auth: Public sealed (rate: 30/min)
{
"username": "player1",
"password": "••••••••",
"hwid": "optional",
"nonce": "<base64 ~16 bytes>",
"timestamp": 1720000001,
"v": 1
}Auth: Public sealed (rate: 30/min)
No password — username + new key. New key's subscriptionTier must be greater than the user's current tier (no linked license → current = 0). Otherwise TIER_NOT_HIGHER.
{
"username": "player1",
"licenseKey": "SDKY-....",
"hwid": "optional",
"nonce": "<base64 ~16 bytes>",
"timestamp": 1720000001,
"v": 1
}Metrics
Auth: Required
App counts, license status tallies, validations last 24h.
Account
Auth: Required
Body: mask (string or null). Default license key mask.
Auth: Required (rate: 20/min)
Body: mode (default | private), password.
Auth: Required (rate: 20/min)
Body: vault check blob fields + password. Used for Private-mode note encryption in the browser.
Auth: Required
List key metadata (no plaintext).
Auth: Required
Soft-revoke a key.
Auth: Required (rate: 20/min)
Body: optional name, optional email, required password. Rectification of developer account data.
Auth: Required (rate: 20/min)
Portable JSON export of the developer account, apps, licenses (no plaintext keys), users, bans, recent logs, and API key metadata. { success, export }.
Auth: Required (rate: 20/min)
Body: password, confirm: "DELETE". Cascades applications and related rows; clears session cookie.
Rate limits
auth 20 / min session 60 / min validate 120 / min clientAuth 30 / min (register / login / upgrade) licenseCreate 30 / min blindCreate 30 / min accountSensitive 20 / min apiKeyAuth 30 / min (failed Bearer attempts)