SDKey

Docs

Sign in

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 (plus GET /health)
  • Dashboard / tooling auth: cookie sdkey_session (EdDSA JWT, 7 days) or Authorization: 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

GET/health

Auth: Public

{ "ok": true, "service": "sdkey-api" }

Auth (developer)

POST/api/v1/auth/register

Auth: Public (rate: 20/min)

Body: email, password (12–128), name, turnstileToken (Cloudflare Turnstile). Sets session cookie. 201 { success, developer }.

POST/api/v1/auth/login

Auth: Public (rate: 20/min)

Body: email, password, turnstileToken. Sets session cookie.

POST/api/v1/auth/logout

Auth: Public

Clears session cookie.

GET/api/v1/auth/me

Auth: Cookie or Bearer

{ success, developer }

Apps

All routes require cookie or Bearer.

POST/api/v1/apps/create

Auth: Required

Body: name, optional version (default 1.0.0). Returns application + sdkHint; embed publicKeyB64 in clients. Defaults: hwidLockEnabled: true, requireLicenseToRegister: true, resolved responseMessages.

GET/api/v1/apps/

Auth: Required

{ success, applications }

GET/api/v1/apps/:id

Auth: Required

Single application owned by the developer (includes settings fields and resolved message map).

DELETE/api/v1/apps/:id

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.

PATCH/api/v1/apps/:id/settings

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.

GET/api/v1/apps/:id/bans

Auth: Required

List HWID/IP bans for the app: { success, bans } — each ban has id, banType (hwid | ip), value, sourceLicenseId, createdAt.

DELETE/api/v1/apps/:id/bans/:banId

Auth: Required

Remove a ban row. Does not unban the license status — use only to clear HWID/IP ban entries. { success: true }.

GET/api/v1/apps/:id/users

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 }.

POST/api/v1/apps/:id/users

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-...."
}
GET/api/v1/apps/:id/users/export

Auth: Required

Full end-user export for data-subject / portability workflows (controller assistance). { success, export }. See Data protection.

DELETE/api/v1/apps/:id/users/:userId

Auth: Required

Erase an app_users row. Does not revoke the linked license or purge historical security logs. Logs user_deleted.

POST/api/v1/apps/:id/users/:userId/reset-hwid

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.

GET/api/v1/apps/:id/licenses

Auth: Required

Query: limit, cursor, optional q. Paginated license list (includes subscriptionTier, lastIp).

POST/api/v1/apps/:id/licenses/extend

Auth: Required

Body: seconds (positive, max ~10 years). Extends active licenses for the app.

GET/api/v1/apps/:id/logs

Auth: Required

Query: limit, cursor, optional q. Security / validation logs.

Licenses (developer)

POST/api/v1/licenses/create

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.

POST/api/v1/licenses/create-blind

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.

POST/api/v1/licenses/:id/ban

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 banned
  • hwid — ban license + insert HWID ban (requires bound HWID)
  • hwid_and_ip — ban license + HWID + IP (IP from body or lastIp)
POST/api/v1/licenses/:id/tier

Auth: Required

Also PATCH. Set any integer tier ≥ 0: { "subscriptionTier": 2 }{ success, subscriptionTier }. Invalidates KV cache.

POST/api/v1/licenses/:id/reset-hwid

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.

DELETE/api/v1/licenses/:id

Auth: Required

Delete a license.

Session & validate (client)

Public SDK endpoints — detailed guide: License control.

POST/api/v1/session/init

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.

POST/api/v1/licenses/validate

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, sessionlicense may be null; never includes password. Opaque session TTL: 7 days. When CRYPTO_ENFORCE=true, plaintext bodies → 400 CRYPTO_REQUIRED.

POST/api/v1/client/register

Auth: Public sealed (rate: 30/min)

Outer envelope: { sessionId, ivB64, ciphertextB64, tagB64 }.

Inner plaintext
{
  "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.

POST/api/v1/client/login

Auth: Public sealed (rate: 30/min)

Inner plaintext
{
  "username": "player1",
  "password": "••••••••",
  "hwid": "optional",
  "nonce": "<base64 ~16 bytes>",
  "timestamp": 1720000001,
  "v": 1
}
POST/api/v1/client/upgrade

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.

Inner plaintext
{
  "username": "player1",
  "licenseKey": "SDKY-....",
  "hwid": "optional",
  "nonce": "<base64 ~16 bytes>",
  "timestamp": 1720000001,
  "v": 1
}

Metrics

GET/api/v1/metrics/overview

Auth: Required

App counts, license status tallies, validations last 24h.

Account

POST/api/v1/account/mask

Auth: Required

Body: mask (string or null). Default license key mask.

POST/api/v1/account/mode

Auth: Required (rate: 20/min)

Body: mode (default | private), password.

POST/api/v1/account/vault

Auth: Required (rate: 20/min)

Body: vault check blob fields + password. Used for Private-mode note encryption in the browser.

POST/api/v1/account/api-keys

Auth: Required

Create key — see API keys.

GET/api/v1/account/api-keys

Auth: Required

List key metadata (no plaintext).

POST/api/v1/account/api-keys/:id/revoke

Auth: Required

Soft-revoke a key.

PATCH/api/v1/account/profile

Auth: Required (rate: 20/min)

Body: optional name, optional email, required password. Rectification of developer account data.

GET/api/v1/account/export

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 }.

POST/api/v1/account/delete

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)