License control
How your program connects to SDKey: open a session, then validate a license key with a hardware id.
What you embed at build time
From the dashboard when you create an application:
API_BASE_URL—https://api.sdkey.dev(no trailing slash)APP_ID— application UUIDAPP_PUBLIC_KEY— Ed25519 public key (32 raw bytes as base64)
The private signing key never leaves the server. Clients only verify with the public key.
Flow
POST /api/v1/session/init— establish an AES session (~15 minutes)POST /api/v1/licenses/validate— seal the license + HWID, verify the signed response
There is no separate activate, deactivate, or heartbeat endpoint. The first successful validate locks the license to that HWID; later validates must match. Re-validate with a fresh nonce when you need another check.
1. Session init
{
"appId": "<uuid>",
"clientNonceB64": "<base64 of 32 random bytes>"
}{
"success": true,
"sessionId": "<uuid>",
"serverNonceB64": "...",
"hkdfSaltB64": "...",
"timestamp": 1720000000,
"signatureB64": "...",
"v": 1
}Verify the Ed25519 signature over canonical JSON of { appId, hkdfSaltB64, serverNonceB64, sessionId, timestamp, v }, then derive the AES-256 session key via HKDF-SHA256. Rate limit: 60 / min / IP.
2. Sealed validate
Call POST https://api.sdkey.dev/api/v1/licenses/validate. Outer HTTPS body:
{
"sessionId": "<uuid>",
"ivB64": "...",
"ciphertextB64": "...",
"tagB64": "..."
}Inner plaintext before AES-GCM seal:
{
"hwid": "MACHINE-1",
"licenseKey": "SDKY-XXXX-XXXX-XXXX-XXXX",
"nonce": "<base64 ~16 bytes>",
"timestamp": 1720000001,
"v": 1
}Response envelope adds signatureB64. Client order is mandatory:
- AES-GCM open → plaintext JSON
- Ed25519 verify with the app public key over canonical JSON
- Check
sessionIdand clock skew (±60s) - Only then honor
success
Rate limit: 120 / min / IP. Use a fresh nonce every call (replay window ~120s).
Success and failure codes
Sealed path success: code: "OK". Common sealed failures (often HTTP 200 with sealed body):
SESSION_EXPIRED CLOCK_SKEW REPLAY LICENSE_NOT_FOUND APP_MISMATCH BANNED EXPIRED HWID_MISMATCH DECRYPT_FAIL APP_DISABLED
First success with an unused key locks HWID, sets status to active, and starts duration if configured. Mismatch later → HWID_MISMATCH.
TypeScript client (self-contained)
There is no published SDK package. Implement the wire protocol in your app with the Web Crypto API (or an equivalent library in C++ / C#). Base64, canonical JSON, Ed25519 verify, HKDF, and AES-GCM seal/open follow the rules on this page.
const API_BASE_URL = 'https://api.sdkey.dev'
const APP_ID = '<uuid>'
const APP_PUBLIC_KEY_B64 = '<base64>' // 32 raw Ed25519 bytes
async function validateLicense(licenseKey: string, hwid: string) {
// 1. Session init
const clientNonce = crypto.getRandomValues(new Uint8Array(32))
const initRes = await fetch(API_BASE_URL + '/api/v1/session/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appId: APP_ID,
clientNonceB64: bytesToBase64(clientNonce),
}),
})
const hello = await initRes.json()
// Verify Ed25519 over canonical JSON of:
// { appId, hkdfSaltB64, serverNonceB64, sessionId, timestamp, v }
// Then HKDF-SHA256 → 32-byte AES session key (see derivation below).
// 2. Sealed validate
const inner = {
hwid,
licenseKey,
nonce: bytesToBase64(crypto.getRandomValues(new Uint8Array(16))),
timestamp: Math.floor(Date.now() / 1000),
v: 1,
}
// AES-GCM seal(inner) → POST /api/v1/licenses/validate
// Open envelope → verify Ed25519 → check sessionId + ±60s skew
// Unlock only when success && code === "OK"
}Session key derivation: IKM = clientNonce || serverNonce (32 + 32 bytes), salt = hkdfSaltB64, info = UTF-8 sdkey-session-v1 + appId, output 32 bytes. Canonical JSON for signatures: object keys sorted lexicographically, no insignificant whitespace.