SDKey

Docs

Sign in

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_URLhttps://api.sdkey.dev (no trailing slash)
  • APP_ID — application UUID
  • APP_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

  1. POST /api/v1/session/init — establish an AES session (~15 minutes)
  2. 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

POST https://api.sdkey.dev/api/v1/session/init
{
  "appId": "<uuid>",
  "clientNonceB64": "<base64 of 32 random bytes>"
}
Response
{
  "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:

  1. AES-GCM open → plaintext JSON
  2. Ed25519 verify with the app public key over canonical JSON
  3. Check sessionId and clock skew (±60s)
  4. 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.

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