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.
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.
Authentication & scoped tokens
Returns metadata about the project the supplied x-project header resolves to. No auth required — use it to confirm your project ID.
curl https://keysystem.nabzclan.vip/api/v1/me \
-H "x-project: p_arctic_fox_92"{
"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:read | GET /keys · GET /keys/:key | List all keys or inspect a single key |
| keys:write | POST /keys/generate · PATCH /keys/:key (field edits) | Mint keys, edit label/expiry/scopes/metadata/HWID |
| keys:revoke | POST /keys/revoke · PATCH /keys/:key (status changes) | Revoke or pause keys |
| logs:read | (reserved) | Read usage logs — for future log export endpoint |
# 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
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.
| unauthorized | 401 | Missing or invalid x-project / secret |
| forbidden | 403 | Project paused, or surface mismatch |
| not_found | 404 | Resource doesn't exist |
| rate_limited | 429 | Too many requests |
| invalid_key | 200 | Key string isn't recognized |
| expired | 200 | Key has expired |
| hwid_mismatch | 200 | HWID didn't match bound device |
| activation_limit | 200 | Seat cap reached |
| checkpoints_required | 200 | Outstanding checkpoint(s) |
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 key
The bread and butter. Send the key value and (for script / license surfaces) a fingerprint. Returns a verdict.
request body
{
"key": "KS-7H2P-VLNX-9KJ4",
"hwid": "first-time-seen-device-id", // script surfaces
"machine_id": "fingerprint-string" // license surfaces
}example call
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
{
"ok": true,
"valid": true,
"key_id": "k_xxxxxxxxxxxx",
"type": "script",
"expires_at": "2026-05-19T07:34:12.000Z",
"scopes": [],
"metadata": null
}rejection example
{
"ok": true,
"valid": false,
"reason": "hwid_mismatch"
}Generate keys (admin)
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
| count | int 1–500 | Number of keys to mint (default: 1) |
| label | string | Human-readable label for the batch |
| ttl_minutes | int | Expiry from now in minutes (0 = never) |
| max_activations | int | License: seat cap (default: 1) |
| max_uses | int | Total validation use cap |
| rate_limit_per_minute | int | API keys: per-key rate limit |
| scopes | string[] | Scopes to attach (API surfaces) |
| hwid | string | Pre-bind to a hardware ID (script) |
| metadata | object | Arbitrary JSON stored on the key |
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
}'{
"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 (admin)
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
| page | int ≥ 1 · default 1 | Page number (1-based) |
| limit | int 1–100 · default 20 | Keys per page |
| status | active · expired · revoked · paused | Filter by key status (omit for all) |
# 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
{
"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 (admin)
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
| label | string | null | Rename key, or null to clear |
| status | active | paused | revoked | Change key state (cannot set expired — system-managed) |
| expires_at | ISO8601 | null | Set an absolute future expiry. Must be in the future. null = make perpetual. |
| ttl_minutes | int ≥ 0 | Set expiry to now + N minutes (0 = make perpetual). Ignored if expires_at is provided. |
| extend_minutes | int ≥ 1 | Add N minutes to the current expiry — safe for renewals, never shortens a key. Ignored if expires_at or ttl_minutes provided. |
| max_uses | int ≥ 1 | null | Update or remove the total use cap (null = unlimited) |
| max_activations | int ≥ 1 | null | Update or remove the license seat cap (null = unlimited) |
| rate_limit_per_minute | int ≥ 1 | null | Update or remove the per-key rate limit (null = no limit) |
| scopes | string[] | Replace the scopes array |
| metadata | object | null | Replace metadata, or null to clear |
| hwid | string | null | null 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_atmust be a future timestamp — past dates return 400.ttl_minutes: 0clears the expiry (makes key perpetual).extend_minutesadds to current expiry if not yet expired, otherwise adds to now.max_uses,max_activations,rate_limit_per_minutemust be ≥ 1 — usenullto remove the limit.
# 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
{
"ok": true,
"key": "KS-7H2P-VLNX-9KJ4",
"id": "k_xxxxxxxxxxxx",
"updated": ["expires_at"]
}Revoke key (admin)
Mark a key as revoked. Subsequent validates return reason: "revoked".
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 key (admin)
Returns the full record for a single key.
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 (script)
For script projects with checkpoints enabled, mark a checkpoint as cleared by slug. Idempotent — re-completing returns successfully without double-counting.
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"}'License activation
Register a machine fingerprint to a license key. Subject to the key's max_activations.
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"}'License deactivation
Remove a machine fingerprint, freeing a seat.
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 file
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
| file | File — required | The source file to obfuscate |
| language | string — optional | Override auto-detection: javascript · lua · php · html · css · python · json |
| strength | string — optional | low · medium (default) · high |
| strip_comments | true/false | Strip comments (all languages) |
| encrypt_strings | true/false | Lua: XOR-encrypt string literals |
| rename_locals | true/false | Lua: rename local variables |
| add_anti_tamper | true/false | Lua: embed anti-tamper checksum |
| disable_console | true/false | JS: disable console.* on load |
| self_defend | true/false | JS: break if script is beautified |
| obfuscate_inline_scripts | true/false | HTML: obfuscate inline <script> tags |
| multi_pass | 1–5 | PHP / Python: number of wrapping passes |
strength levels
| low | Lua | Strip comments + minify whitespace only. No string encryption, no renaming. |
| medium | Lua | XOR-encrypt all string literals with a random key. Decoder injected at top. Minified. |
| high | Lua | Medium + rename local variables + wrap entire payload in a second XOR loadstring shell. |
| low | JavaScript | Compact + base64 string array (60% threshold) + mangled identifier names. |
| medium | JavaScript | Control-flow flattening (40%) + dead code injection + hex identifiers + split strings + object key transforms. |
| high | JavaScript | Everything from medium + debug protection + RC4 string encoding (100%) + unicode escape + self-defending + numbers-to-expressions. |
| low | PHP | 1-pass eval(gzinflate(base64_decode(...))) wrap. |
| medium | PHP | 2-pass wrap + str_rot13 salt between passes. |
| high | PHP | 3-pass wrap + str_rot13 + strrev + character-shifted decoder. |
| low | Python | 1-pass exec(zlib.decompress(base64.b64decode(...))) wrap. |
| medium | Python | 2-pass nested exec + zlib + base64. |
| high | Python | 3-pass nested exec + zlib + base64. |
| low / medium | CSS | Strip comments + aggressively minify whitespace. |
| high | CSS | Minify + rename all class and ID selectors to random short names. |
| low | JSON | Minify + base64 envelope: {v,format,payload}. |
| medium / high | JSON | Minify + XOR + base64 envelope with key embedded. High uses a 24-byte key vs 12-byte. |
| low | HTML | Minify + entity-encode text nodes. |
| medium | HTML | Minify + entity-encode + obfuscate inline <script> blocks (if enabled). |
| high | HTML | Everything from medium + base64 outer document wrap. |
example call
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.luaresponse headers
| Content-Disposition | attachment; filename="obfuscated_script.lua" |
| X-Input-Bytes | original file size in bytes |
| X-Output-Bytes | obfuscated file size in bytes |
| X-Duration-Ms | server-side processing time |
| X-Language | language used (confirmed or auto-detected) |
error codes
| unauthorized | 401 | Missing or invalid Bearer token |
| forbidden | 403 | Free tier — upgrade to VIP Tier 1+ to use the API |
| bad_request | 400 | No file field, unrecognised language, or bad content type |
| forbidden | 403 | File exceeds your plan's max file size |
| rate_limited | 429 | Monthly obfuscation quota reached for your plan |
| obfuscation_failed | 422 | Engine error — check that the source is valid for the language |
auto-detected extensions
| .js / .mjs / .cjs | javascript |
| .lua | lua |
| .php | php |
| .html / .htm | html |
| .css | css |
| .py | python |
| .json | json |
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.
{
"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 · typekey.validatedValidate call succeededkey_id · ipkey.revokedKey manually revokedkey_idkey.expiredKey checked after its TTL elapsedkey_idkey.activatedLicense seat consumed (machine activation)key_id · machine_idkey.deactivatedLicense seat freed (machine deactivated)key_id · machine_idcheckpoint.completeScript checkpoint cleared for a keykey_id · checkpoint (slug)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.
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
}
}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<?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 use cases
Common patterns teams build on top of Keystation webhooks:
key.expiredkey.validatedkey.revokedkey.activated · key.deactivatedcheckpoint.completekey.generatedFor complete, copy-paste webhook handler code in Node.js, Python, and PHP — see the Examples page.
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.