The Keystation API.
A single REST surface for validating, minting and revoking keys across all three key types. All endpoints accept JSON, return JSON, and live under https://keysystem.nabzclan.vip/api/v1.
Overview
Every API request requires an x-project header carrying your project's public ID (the short string visible at the top of every project page). Validation calls require only this header. Administrative calls additionally require a Authorization: Bearer <api_secret> header — the secret lives under the project's Settings tab.
We never accept the project secret over query parameters. All traffic must be HTTPS. CORS is enabled — but for any administrative endpoint you should be calling from your server, never your client.
Authentication
Returns metadata about the project the supplied x-project header resolves to.
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
}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" }
]
}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"}'Webhook events
Subscribe from the project's Webhooks tab. Each delivery is a POST containing:
{
"event": "key.validated",
"sent_at": "2026-05-11T10:23:01.124Z",
"data": { "key_id": "k_xxx", "ip": "1.2.3.4" }
}| key.generated | Key was minted |
| key.validated | A validate call succeeded |
| key.rejected | A validate call failed |
| key.revoked | Key was revoked |
| key.expired | Key crossed its TTL |
| key.activated | License seat consumed |
| key.deactivated | License seat freed |
| checkpoint.complete | Script checkpoint cleared |
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.
import crypto from "node:crypto";
export function verifyWebhook(body: string, header: string, secret: string): boolean {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(body).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
} catch {
return false; // length mismatch
}
}import hmac, hashlib
def verify_webhook(body: str, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(header, expected)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.