/ api · v1

The Keystation API.

A single REST surface for validating, minting and revoking keys across all three key types — plus a file obfuscation endpoint for VIP members. All endpoints live under https://keysystem.nabzclan.vip/api/v1.

Examples cookbook
/ overview

Overview

Every API request requires an x-projectheader carrying your project's public ID (visible at the top of every project page). Validation calls require only this header. Administrative calls (generate, list, edit, revoke, inspect) require auth via one of two methods:

Project API secretFull access to all admin endpoints. Found under project Settings → API Secret. Never expose client-side.
Scoped token (kss_…)Least-privilege token issued from the project Credentials tab. Each token has specific permissions — e.g. keys:read, keys:write, keys:revoke. Use these for bots, CI, or third-party integrations.

Pass either via Authorization: Bearer <token>. We never accept secrets over query parameters. All traffic must be HTTPS.

/ auth

Authentication & scoped tokens

GET/api/v1/me

Returns metadata about the project the supplied x-project header resolves to. No auth required — use it to confirm your project ID.

curl
curl https://keysystem.nabzclan.vip/api/v1/me \
  -H "x-project: p_arctic_fox_92"
response.json
{
  "ok": true,
  "project_id": "p_arctic_fox_92",
  "name": "Arctic Fox Hub",
  "type": "script",
  "is_active": true
}

scoped token permissions

Create scoped kss_… tokens in your project's Credentials tab. Each token carries one or more permissions. The full API secret grants all permissions unconditionally.

keys:readGET /keys · GET /keys/:keyList all keys or inspect a single key
keys:writePOST /keys/generate · PATCH /keys/:key (field edits)Mint keys, edit label/expiry/scopes/metadata/HWID
keys:revokePOST /keys/revoke · PATCH /keys/:key (status changes)Revoke or pause keys
logs:read(reserved)Read usage logs — for future log export endpoint
curl — using a scoped token
# kss_ token with keys:read — can list and inspect, cannot generate
curl "https://keysystem.nabzclan.vip/api/v1/keys?status=active" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer kss_<your_scoped_token>"

# kss_ token with keys:write — can generate and edit, cannot revoke
curl -X POST "https://keysystem.nabzclan.vip/api/v1/keys/generate" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer kss_<your_scoped_token>" \
  -H "Content-Type: application/json" \
  -d '{"count": 5, "label": "batch-june"}'
/ errors

Errors

All errors return JSON with ok: false and an error code. Validation failures (invalid key, expired, hwid mismatch, etc.) still return HTTP 200 — only transport or authorization problems return 4xx/5xx.

unauthorized401Missing or invalid x-project / secret
forbidden403Project paused, or surface mismatch
not_found404Resource doesn't exist
rate_limited429Too many requests
invalid_key200Key string isn't recognized
expired200Key has expired
hwid_mismatch200HWID didn't match bound device
activation_limit200Seat cap reached
checkpoints_required200Outstanding checkpoint(s)
/ rate-limits

Rate limits

The validate endpoint is rate-limited at 240 req/min per (project, ip) pair. Keys with a configured rate_limit_per_minute apply on top. Hit the limit and you'll get HTTP 429 with a retry-after hint.

/ validate

Validate key

POST/api/v1/keys/validate

The bread and butter. Send the key value and (for script / license surfaces) a fingerprint. Returns a verdict.

request body

application/json
{
  "key": "KS-7H2P-VLNX-9KJ4",
  "hwid": "first-time-seen-device-id",      // script surfaces
  "machine_id": "fingerprint-string"        // license surfaces
}

example call

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/keys/validate \
  -H "x-project: p_arctic_fox_92" \
  -H "Content-Type: application/json" \
  -d '{"key":"KS-7H2P-VLNX-9KJ4","hwid":"7a3f2e9b"}'

response

200 ok
{
  "ok": true,
  "valid": true,
  "key_id": "k_xxxxxxxxxxxx",
  "type": "script",
  "expires_at": "2026-05-19T07:34:12.000Z",
  "scopes": [],
  "metadata": null
}

rejection example

200 ok · rejection
{
  "ok": true,
  "valid": false,
  "reason": "hwid_mismatch"
}
/ generate

Generate keys (admin)

POST/api/v1/keys/generaterequires project secret

Mint one or more keys server-side. Inherits project defaults if fields are omitted. Always call from your server — never expose your API secret client-side.

request body fields

countint 1–500Number of keys to mint (default: 1)
labelstringHuman-readable label for the batch
ttl_minutesintExpiry from now in minutes (0 = never)
max_activationsintLicense: seat cap (default: 1)
max_usesintTotal validation use cap
rate_limit_per_minuteintAPI keys: per-key rate limit
scopesstring[]Scopes to attach (API surfaces)
hwidstringPre-bind to a hardware ID (script)
metadataobjectArbitrary JSON stored on the key
curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/keys/generate \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "count": 5,
    "label": "promo-friday",
    "ttl_minutes": 10080,
    "scopes": ["read", "write"],
    "rate_limit_per_minute": 60
  }'
200 ok
{
  "ok": true,
  "count": 5,
  "keys": [
    { "id": "k_a1b2c3", "key": "KS-7H2P-VLNX-9KJ4", "expires_at": "2026-06-01T00:00:00Z" },
    { "id": "k_d4e5f6", "key": "KS-G8XX-LM34-ZZ91", "expires_at": "2026-06-01T00:00:00Z" }
  ]
}
/ list-keys

List keys (admin)

GET/api/v1/keysrequires project secret

Returns a paginated list of all keys for the project, newest first. Key objects are identical to the inspect endpoint so you can use the same parser for both.

query parameters

pageint ≥ 1 · default 1Page number (1-based)
limitint 1–100 · default 20Keys per page
statusactive · expired · revoked · pausedFilter by key status (omit for all)
curl
# Page 1, 20 per page
curl "https://keysystem.nabzclan.vip/api/v1/keys?page=1&limit=20" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>"

# Only active keys, page 2
curl "https://keysystem.nabzclan.vip/api/v1/keys?status=active&page=2&limit=50" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>"

response

200 ok
{
  "ok": true,
  "keys": [
    {
      "key": "KS-7H2P-VLNX-9KJ4",
      "id": "k_xxxxxxxxxxxx",
      "status": "active",
      "label": "giveaway-batch",
      "hwid": null,
      "machine_ids": [],
      "max_activations": null,
      "current_activations": 0,
      "scopes": [],
      "rate_limit_per_minute": null,
      "current_uses": 42,
      "max_uses": null,
      "expires_at": "2026-12-31T00:00:00.000Z",
      "last_used_at": "2026-05-14T10:00:00.000Z",
      "last_used_ip": "1.2.3.4",
      "created_at": "2026-05-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "pages": 8,
    "has_next": true,
    "has_prev": false
  }
}
/ edit-key

Edit key (admin)

PATCH/api/v1/keys/{key}requires project secret

Update one or more mutable fields on a key. All fields are optional — only the fields you send are changed. Returns the list of fields that were updated.

Permission: keys:write for field edits; keys:revoke when status is included.

request body fields

labelstring | nullRename key, or null to clear
statusactive | paused | revokedChange key state (cannot set expired — system-managed)
expires_atISO8601 | nullSet an absolute future expiry. Must be in the future. null = make perpetual.
ttl_minutesint ≥ 0Set expiry to now + N minutes (0 = make perpetual). Ignored if expires_at is provided.
extend_minutesint ≥ 1Add N minutes to the current expiry — safe for renewals, never shortens a key. Ignored if expires_at or ttl_minutes provided.
max_usesint ≥ 1 | nullUpdate or remove the total use cap (null = unlimited)
max_activationsint ≥ 1 | nullUpdate or remove the license seat cap (null = unlimited)
rate_limit_per_minuteint ≥ 1 | nullUpdate or remove the per-key rate limit (null = no limit)
scopesstring[]Replace the scopes array
metadataobject | nullReplace metadata, or null to clear
hwidstring | nullnull clears the HWID binding — allows the key to re-bind on next validate

expiry field priority

When multiple expiry fields are sent, expires_at wins over ttl_minutes, which wins over extend_minutes.

validation rules

  • expires_at must be a future timestamp — past dates return 400.
  • ttl_minutes: 0 clears the expiry (makes key perpetual).
  • extend_minutes adds to current expiry if not yet expired, otherwise adds to now.
  • max_uses, max_activations, rate_limit_per_minute must be ≥ 1 — use null to remove the limit.
curl
# Add 30 days to existing expiry — safe renewal, never shortens
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"extend_minutes": 43200}'

# Set an absolute expiry date
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "customer-alice-renewed",
    "expires_at": "2027-01-01T00:00:00Z"
  }'

# Set expiry from now (replaces current expiry — use extend_minutes to add instead)
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"ttl_minutes": 10080}'

# Remove expiry entirely — make perpetual
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"ttl_minutes": 0}'

# Pause a key
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

# Clear HWID binding (let key re-bind to a new device)
curl -X PATCH "https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4" \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"hwid": null}'

response

200 ok
{
  "ok": true,
  "key": "KS-7H2P-VLNX-9KJ4",
  "id": "k_xxxxxxxxxxxx",
  "updated": ["expires_at"]
}
/ revoke

Revoke key (admin)

POST/api/v1/keys/revokerequires project secret

Mark a key as revoked. Subsequent validates return reason: "revoked".

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/keys/revoke \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>" \
  -H "Content-Type: application/json" \
  -d '{"key": "KS-7H2P-VLNX-9KJ4"}'
/ inspect

Inspect key (admin)

GET/api/v1/keys/{key}requires project secret

Returns the full record for a single key.

curl
curl https://keysystem.nabzclan.vip/api/v1/keys/KS-7H2P-VLNX-9KJ4 \
  -H "x-project: p_arctic_fox_92" \
  -H "Authorization: Bearer <api_secret>"
/ checkpoints

Checkpoints (script)

POST/api/v1/checkpoints/complete

For script projects with checkpoints enabled, mark a checkpoint as cleared by slug. Idempotent — re-completing returns successfully without double-counting.

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/checkpoints/complete \
  -H "x-project: p_arctic_fox_92" \
  -H "Content-Type: application/json" \
  -d '{"key":"KS-7H2P-VLNX-9KJ4","checkpoint":"step-1-linkvertise"}'
/ activate

License activation

POST/api/v1/license/activate

Register a machine fingerprint to a license key. Subject to the key's max_activations.

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/license/activate \
  -H "x-project: p_arctic_fox_92" \
  -H "Content-Type: application/json" \
  -d '{"key":"ABCD-EFGH-IJKL-MNOP-QRST","machine_id":"fp-string","machine_name":"Dell-XPS-13"}'
/ deactivate

License deactivation

POST/api/v1/license/deactivate

Remove a machine fingerprint, freeing a seat.

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/license/deactivate \
  -H "x-project: p_arctic_fox_92" \
  -H "Content-Type: application/json" \
  -d '{"key":"ABCD-EFGH-IJKL-MNOP-QRST","machine_id":"fp-string"}'
/ obfuscate

Obfuscate file

POST/api/v1/obfuscateVIP Tier 1+ only

Upload any source file and receive the obfuscated version back as a download. Language is detected automatically from the file extension — override it with a language field.

Auth: use a project API token (kss_…) with the obfuscate permission. Generate one in your project's Credentials tab. Requires VIP Tier 1 or higher — free accounts can use the dashboard obfuscator instead.

form fields

fileFile — requiredThe source file to obfuscate
languagestring — optionalOverride auto-detection: javascript · lua · php · html · css · python · json
strengthstring — optionallow · medium (default) · high
strip_commentstrue/falseStrip comments (all languages)
encrypt_stringstrue/falseLua: XOR-encrypt string literals
rename_localstrue/falseLua: rename local variables
add_anti_tampertrue/falseLua: embed anti-tamper checksum
disable_consoletrue/falseJS: disable console.* on load
self_defendtrue/falseJS: break if script is beautified
obfuscate_inline_scriptstrue/falseHTML: obfuscate inline <script> tags
multi_pass1–5PHP / Python: number of wrapping passes

strength levels

lowLuaStrip comments + minify whitespace only. No string encryption, no renaming.
mediumLuaXOR-encrypt all string literals with a random key. Decoder injected at top. Minified.
highLuaMedium + rename local variables + wrap entire payload in a second XOR loadstring shell.
lowJavaScriptCompact + base64 string array (60% threshold) + mangled identifier names.
mediumJavaScriptControl-flow flattening (40%) + dead code injection + hex identifiers + split strings + object key transforms.
highJavaScriptEverything from medium + debug protection + RC4 string encoding (100%) + unicode escape + self-defending + numbers-to-expressions.
lowPHP1-pass eval(gzinflate(base64_decode(...))) wrap.
mediumPHP2-pass wrap + str_rot13 salt between passes.
highPHP3-pass wrap + str_rot13 + strrev + character-shifted decoder.
lowPython1-pass exec(zlib.decompress(base64.b64decode(...))) wrap.
mediumPython2-pass nested exec + zlib + base64.
highPython3-pass nested exec + zlib + base64.
low / mediumCSSStrip comments + aggressively minify whitespace.
highCSSMinify + rename all class and ID selectors to random short names.
lowJSONMinify + base64 envelope: {v,format,payload}.
medium / highJSONMinify + XOR + base64 envelope with key embedded. High uses a 24-byte key vs 12-byte.
lowHTMLMinify + entity-encode text nodes.
mediumHTMLMinify + entity-encode + obfuscate inline <script> blocks (if enabled).
highHTMLEverything from medium + base64 outer document wrap.

example call

curl
curl -X POST https://keysystem.nabzclan.vip/api/v1/obfuscate \
  -H "Authorization: Bearer kss_<your_api_token>" \
  -F "[email protected]" \
  -F "strength=high" \
  -F "encrypt_strings=true" \
  -o obfuscated_script.lua

response headers

Content-Dispositionattachment; filename="obfuscated_script.lua"
X-Input-Bytesoriginal file size in bytes
X-Output-Bytesobfuscated file size in bytes
X-Duration-Msserver-side processing time
X-Languagelanguage used (confirmed or auto-detected)

error codes

unauthorized401Missing or invalid Bearer token
forbidden403Free tier — upgrade to VIP Tier 1+ to use the API
bad_request400No file field, unrecognised language, or bad content type
forbidden403File exceeds your plan's max file size
rate_limited429Monthly obfuscation quota reached for your plan
obfuscation_failed422Engine error — check that the source is valid for the language

auto-detected extensions

.js / .mjs / .cjsjavascript
.lualua
.phpphp
.html / .htmhtml
.csscss
.pypython
.jsonjson
/ webhook-events

Webhook events

Subscribe from the project's Webhooks tab. Each delivery is a signed POST. Your server has 8 seconds to respond with any 2xx. One automatic retry fires 3 seconds after a failure.

payload envelope
{
  "event":   "key.validated",
  "sent_at": "2026-05-14T10:23:01.124Z",
  "data":    { "key_id": "k_abc123", "ip": "1.2.3.4" }
}

event · data fields

key.generatedKey minted via dashboard, API, or reward flowkey_id · project_id · type
key.validatedValidate call succeededkey_id · ip
key.revokedKey manually revokedkey_id
key.expiredKey checked after its TTL elapsedkey_id
key.activatedLicense seat consumed (machine activation)key_id · machine_id
key.deactivatedLicense seat freed (machine deactivated)key_id · machine_id
checkpoint.completeScript checkpoint cleared for a keykey_id · checkpoint (slug)
/ webhook-verify

Verifying signatures

Every webhook includes an x-keystation-signature header containing sha256=<hex> — the HMAC of the raw body using the signing secret shown when the subscription was created. Use a raw body parser so the bytes are identical to what was signed. Never compare with === — use constant-time comparison.

verify.ts
import crypto from "node:crypto";

// Use express.raw() so the body bytes are unmodified
app.use("/webhooks", express.raw({ type: "application/json" }));

export function verifyWebhook(rawBody: Buffer, header: string, secret: string): boolean {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
  } catch {
    return false; // length mismatch → reject
  }
}
verify.py
import hmac, hashlib

def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(header, expected)

# FastAPI / Starlette: use await request.body() — do NOT parse JSON first
verify.php
<?php
$secret    = getenv('KEYSTATION_WEBHOOK_SECRET');
$rawBody   = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_KEYSTATION_SIGNATURE'] ?? '';

$expected  = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $sigHeader)) {
    http_response_code(401);
    exit(json_encode(['error' => 'Bad signature']));
}

$payload = json_decode($rawBody, true);
/ webhook-usecases

Webhook use cases

Common patterns teams build on top of Keystation webhooks:

Discord DM on expiry
key.expired
Look up the user's Discord ID from the key's metadata and send a renewal reminder via your bot.
Audit log / analytics
key.validated
Write every successful validation to your own database for custom retention, geo-analytics, or compliance export.
Revocation cascade
key.revoked
Immediately invalidate any session tokens or cached auth responses tied to the revoked key in your own system.
Seat change sync
key.activated · key.deactivated
Mirror license seat counts into your own user dashboard so customers can see live seat usage without leaving your app.
Funnel analytics
checkpoint.complete
Push each checkpoint completion to Mixpanel / PostHog to measure which ad steps cause drop-off.
Post-purchase provisioning
key.generated
After a reward or store flow mints a key, fire a webhook to create the user's account, send a welcome email, or seed initial data.

For complete, copy-paste webhook handler code in Node.js, Python, and PHP — see the Examples page.

/ more

See it all working end-to-end

The Examples page has complete, copy-paste flows for every key type — Roblox Lua, Node.js middleware, Python license checks, and webhook handlers in four languages.

Browse examples