Skip to main content
FreelanceTimer

License API Documentation

REST endpoints powering desktop-app license activation, verification, and lifecycle management.

Authentication

The customer-facing license endpoints don't require an API key — the request itself includes the license_key and device_fingerprint which are cryptographically equivalent to credentials.

For server-to-server integrations (e.g. a Stripe webhook bridge or Zapier flow), pass an admin-generated API key in the X-API-Key header. API-key requests bypass IP-based rate limits.

Generate API keys in the admin dashboard at /admin/api-keys.

Device fingerprint format

Each device must produce a stable identifier hashed to SHA-256. Format: 64 lowercase hexadecimal characters. Malformed fingerprints are rejected with ERR_MISSING_FIELDS.

// Node / Electron example
const crypto = require('crypto'), os = require('os')
const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
  .flat().find((i) => i && !i.internal)?.mac || '')
const fingerprint = crypto.createHash('sha256').update(seed).digest('hex')

Suggested device_name format: USERNAME-HOSTNAME (e.g. JOHN-LAPTOP).

Security architecture

The license API enforces multiple layers of defence (Sections H1–H9). Each layer is optional in development but strongly recommended in production.

H1 · Gmail-bound activation

Every license is bound to 1–5 specific Gmail addresses at purchase time (1 for Personal, 2 for Pro, 5 for Team). At activation the desktop app must perform a Google OAuth sign-in and POST the resulting id_token. The server validates it via https://oauth2.googleapis.com/tokeninfo and rejects activation if the email does not match. This makes stolen keys unusable without the bound Google account.

H2 · Signed activation tokens

/activate returns an activation_token — a compact JWT-like value (base64url(header).base64url(body).base64url(HMAC-SHA256)) with a 30-day TTL. Subsequent calls to /verify can pass it in the X-Activation-Token header for a fast path that skips the database lookup. The token contains {lid, fp, gmail, kid, iat, exp}.

H3 · Heartbeat challenge/response

Each heartbeat must include a fresh nonce + HMAC proof:

// 1. GET /api/license/heartbeat-challenge
//    → { nonce, expires_in: 60, server_time }
// 2. Compute proof client-side
const crypto = require('crypto')
const proof = crypto
  .createHmac('sha256', activation_token)
  .update(nonce + license_key + device_fingerprint)
  .digest('base64url')

// 3. POST /api/license/heartbeat { nonce, proof, ... }

Replayed heartbeats are rejected because each nonce is consumed on first use.

H4 · Suspicious activity detection

The server runs 5 detection rules on each activation: multi_ip (3+ IPs in 24h), multi_country (2+ countries in 7d), multi_device_same_ip (2+ devices from one IP in 1h), rapid_activation (5+ in 60s), and impossible_travel. High-severity events automatically flip the license to review_required (read-only) and bump check intervals to 1 hour.

H5 · Read-only graceful degradation

Heartbeat responses include a mode field:

  • normal — full functionality
  • warning — license valid but expires soon; show a banner
  • read_only — license is review_required, revoked, refunded, or expired. The desktop app should preserve all local data and disable editing/exporting features. Never set force_logout — let users keep reading their data.

H9 · Response signing (X-Signature)

Every JSON response carries an X-Signature header (HMAC-SHA256 of the raw response body, base64url encoded) and an X-Signing-Kid header (active key id). Clients with the shared LICENSE_SIGNING_KEY can verify the response was not tampered with.

// Node.js verification example
const crypto = require('crypto')

async function verifyResponse(res, sharedKey) {
  const body = await res.text()
  const sig  = res.headers.get('x-signature')
  const kid  = res.headers.get('x-signing-kid')
  const expected = crypto.createHmac('sha256', sharedKey)
    .update(body).digest('base64url')
  if (sig !== expected) {
    throw new Error('Response signature mismatch — possible tampering!')
  }
  return JSON.parse(body)  // Body is already JSON-parsed for you
}

The LICENSE_SIGNING_KEY is a server-only secret. Embed a copy in your desktop app via build-time obfuscation if you want offline verification — or skip verification and rely on TLS alone.

Endpoints

POST/api/license/activate5 requests / hour per IP

Activate license on a device (Gmail-bound)

Binds a license key to a device fingerprint AND to the user's Gmail account (Section H1). The desktop app must perform a Google sign-in and pass the resulting id_token in the body as google_id_token. The server verifies the token with Google and checks the Gmail matches the address(es) bound to this license at purchase time. Returns a signed activation_token (H2) used for fast verify and heartbeat proofs.

Request body

{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "gmail_address": "user@gmail.com",
  "google_id_token": "<JWT obtained via Google Sign-In>",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "device_name": "JOHN-LAPTOP",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
}

Success response (200)

{
  "success": true,
  "message": "License activated successfully.",
  "status": "active",
  "mode": "normal",
  "plan": "pro",
  "max_devices": 2,
  "used_devices": 1,
  "bound_gmail": "user@gmail.com",
  "activation_token": "<JWT-like signed token, 30-day TTL>",
  "activation_token_expires_at": "2026-06-28T10:00:00Z",
  "next_check_in_hours": 6,
  "is_lifetime": true,
  "renewal_date": null
}
Code examples
curl
curl -X POST 'https://freelancetimer-license.pages.dev/api/license/activate' \
  -H 'Content-Type: application/json' \
  -d '{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "gmail_address": "user@gmail.com",
  "google_id_token": "<JWT obtained via Google Sign-In>",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "device_name": "JOHN-LAPTOP",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
}'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/activate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "gmail_address": "user@gmail.com",
  "google_id_token": "<JWT obtained via Google Sign-In>",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "device_name": "JOHN-LAPTOP",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
})
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/activate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "gmail_address": "user@gmail.com",
  "google_id_token": "<JWT obtained via Google Sign-In>",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "device_name": "JOHN-LAPTOP",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
},
    device_fingerprint: deviceFingerprint()
  })
})
const data = await res.json()
POST/api/license/verify10 requests / minute per IP

Verify license + device pair

Light validity check. Use on app startup. If you pass the X-Activation-Token header (H2), the server can validate without a DB lookup (fast path).

Request headers

X-Activation-Token: <optional: signed token from /activate response>

Request body

{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "app_version": "1.0.0"
}

Success response (200)

{
  "success": true,
  "message": "License valid for this device.",
  "status": "active",
  "mode": "normal",
  "plan": "pro",
  "max_devices": 2,
  "used_devices": 1,
  "expires_at": null,
  "is_lifetime": true,
  "renewal_date": null
}
Code examples
curl
curl -X POST 'https://freelancetimer-license.pages.dev/api/license/verify' \
  -H 'X-Activation-Token: <optional: signed token from /activate response>' \
  -H 'Content-Type: application/json' \
  -d '{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "app_version": "1.0.0"
}'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/verify', {
  method: 'POST',
  headers: {
    'X-Activation-Token': '<optional: signed token from /activate response>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "app_version": "1.0.0"
})
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "app_version": "1.0.0"
},
    device_fingerprint: deviceFingerprint()
  })
})
const data = await res.json()
GET/api/license/heartbeat-challenge120 requests / hour per IP

Get a heartbeat nonce (H3)

Returns a server-issued random nonce (default 60s TTL). The client must compute HMAC-SHA256(activation_token, nonce + license_key + device_fingerprint) and POST it back to /heartbeat as `proof`. This prevents replay attacks even if a heartbeat response is captured.

Success response (200)

{
  "success": true,
  "nonce": "<32-char random hex>",
  "expires_in": 60,
  "server_time": "2026-05-29T10:00:00Z"
}
Code examples
curl
curl -X GET 'https://freelancetimer-license.pages.dev/api/license/heartbeat-challenge'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/heartbeat-challenge', {
  method: 'GET',
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/heartbeat-challenge', {
  method: 'GET',
  
})
const data = await res.json()
GET/api/license/status30 requests / minute per IP

Get full license info for Settings page

Read-only. Returns everything the desktop app needs to display in its License Settings panel. Does NOT increment usage or modify the activation row.

Request headers

X-License-Key: FT-XXXX-XXXX-XXXX-XXXX
X-Device-Fingerprint: <64-char SHA-256 hex>

Success response (200)

{
  "success": true,
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "status": "active",
  "plan": "pro",
  "max_devices": 3,
  "used_devices": 1,
  "is_lifetime": true,
  "renewal_date": null,
  "customer_email": "user@example.com",
  "customer_name": "John Doe",
  "activated_on_this_device": true,
  "device_name": "JOHN-LAPTOP",
  "activated_at": "2026-05-15T10:30:00Z",
  "last_verified_at": "2026-05-16T08:00:00Z",
  "app_version_on_record": "1.0.0",
  "support_email": "support@yourdomain.com",
  "server_time": "2026-05-16T10:00:00Z"
}
Code examples
curl
curl -X GET 'https://freelancetimer-license.pages.dev/api/license/status' \
  -H 'X-License-Key: FT-XXXX-XXXX-XXXX-XXXX' \
  -H 'X-Device-Fingerprint: <64-char SHA-256 hex>'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/status', {
  method: 'GET',
  headers: {
    'X-License-Key': 'FT-XXXX-XXXX-XXXX-XXXX',
    'X-Device-Fingerprint': '<64-char SHA-256 hex>'
  },
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/status', {
  method: 'GET',
  headers: {
    ...{
  "X-License-Key": "FT-XXXX-XXXX-XXXX-XXXX",
  "X-Device-Fingerprint": "<64-char SHA-256 hex>"
},
    'X-Device-Fingerprint': deviceFingerprint()
  }
})
const data = await res.json()
POST/api/license/heartbeat60 requests / hour per device fingerprint

Periodic heartbeat with HMAC proof (H3 + H5)

Updates last_seen_at + app_version. Now requires a nonce + proof obtained via /heartbeat-challenge (replay protection). Returns a `mode` field — "normal", "warning" (close to expiry), or "read_only" (license under review / revoked / expired — do NOT force logout, just render the app read-only). next_check_in_hours is adaptive: 6h for new devices, 24h for established ones, 1h when suspicious activity is flagged.

Request body

{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "activation_token": "<from /activate response>",
  "nonce": "<from /heartbeat-challenge>",
  "proof": "<HMAC-SHA256(activation_token, nonce+license_key+device_fingerprint), b64url>",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
}

Success response (200)

{
  "valid": true,
  "force_logout": false,
  "mode": "normal",
  "message": "OK",
  "next_check_in_hours": 24,
  "server_time": "2026-05-29T10:00:00Z",
  "activation_token": "<refreshed token if <5 days remaining, else absent>",
  "is_lifetime": true,
  "renewal_date": null
}
Code examples
curl
curl -X POST 'https://freelancetimer-license.pages.dev/api/license/heartbeat' \
  -H 'Content-Type: application/json' \
  -d '{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "activation_token": "<from /activate response>",
  "nonce": "<from /heartbeat-challenge>",
  "proof": "<HMAC-SHA256(activation_token, nonce+license_key+device_fingerprint), b64url>",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
}'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/heartbeat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "activation_token": "<from /activate response>",
  "nonce": "<from /heartbeat-challenge>",
  "proof": "<HMAC-SHA256(activation_token, nonce+license_key+device_fingerprint), b64url>",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
})
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/heartbeat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "activation_token": "<from /activate response>",
  "nonce": "<from /heartbeat-challenge>",
  "proof": "<HMAC-SHA256(activation_token, nonce+license_key+device_fingerprint), b64url>",
  "app_version": "1.0.0",
  "os_info": "Windows 11"
},
    device_fingerprint: deviceFingerprint()
  })
})
const data = await res.json()
GET/api/license/public-keyunrestricted

Get response-signing metadata (H9)

Returns metadata about the HMAC algorithm, active key id (kid), and TTLs the server is using to sign responses. The actual HMAC secret is NEVER exposed — both server and client need to share a pre-provisioned LICENSE_SIGNING_KEY to verify signatures. Useful for clients to introspect server config.

Success response (200)

{
  "algorithm": "HS256",
  "kid": "k1",
  "signed_responses": true,
  "activation_token_ttl_days": 30,
  "challenge_ttl_seconds": 60,
  "notes": "X-Signature on every JSON response is HMAC-SHA256(LICENSE_SIGNING_KEY, raw_body) encoded base64url."
}
Code examples
curl
curl -X GET 'https://freelancetimer-license.pages.dev/api/license/public-key'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/public-key', {
  method: 'GET',
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/public-key', {
  method: 'GET',
  
})
const data = await res.json()
POST/api/license/deactivate30 requests / minute per IP

Release a device slot

Call when the user signs out / uninstalls. Frees a max_devices slot for re-use.

Request body

{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>"
}

Success response (200)

{
  "success": true,
  "message": "Device deactivated.",
  "status": "ok",
  "plan": "pro",
  "max_devices": 3,
  "used_devices": 0
}
Code examples
curl
curl -X POST 'https://freelancetimer-license.pages.dev/api/license/deactivate' \
  -H 'Content-Type: application/json' \
  -d '{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>"
}'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/deactivate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>"
})
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/deactivate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "device_fingerprint": "<64-char SHA-256 hex>"
},
    device_fingerprint: deviceFingerprint()
  })
})
const data = await res.json()
POST/api/license/request-transfer10 requests / 10 minutes per IP

Request a device transfer

Used when a user has reached max_devices and wants to switch to a new machine. Creates a pending transfer_request for admin review.

Request body

{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "customer_email": "user@example.com",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "new_device_name": "JOHN-DESKTOP",
  "reason": "Replaced my laptop"
}

Success response (200)

{
  "success": true,
  "message": "Transfer request submitted. An admin will review it shortly.",
  "status": "transfer_pending",
  "transfer_request_id": 42
}
Code examples
curl
curl -X POST 'https://freelancetimer-license.pages.dev/api/license/request-transfer' \
  -H 'Content-Type: application/json' \
  -d '{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "customer_email": "user@example.com",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "new_device_name": "JOHN-DESKTOP",
  "reason": "Replaced my laptop"
}'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/request-transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "customer_email": "user@example.com",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "new_device_name": "JOHN-DESKTOP",
  "reason": "Replaced my laptop"
})
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/request-transfer', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...{
  "license_key": "FT-XXXX-XXXX-XXXX-XXXX",
  "customer_email": "user@example.com",
  "device_fingerprint": "<64-char SHA-256 hex>",
  "new_device_name": "JOHN-DESKTOP",
  "reason": "Replaced my laptop"
},
    device_fingerprint: deviceFingerprint()
  })
})
const data = await res.json()
GET/api/license/healthunrestricted

Health check

Simple ping endpoint. No rate limit. Useful for app connectivity tests.

Success response (200)

{
  "ok": true,
  "time": "2026-05-16T10:00:00Z"
}
Code examples
curl
curl -X GET 'https://freelancetimer-license.pages.dev/api/license/health'
JavaScript fetch
const res = await fetch('https://freelancetimer-license.pages.dev/api/license/health', {
  method: 'GET',
})
const data = await res.json()
if (!data.success) console.error('error_code:', data.error_code)
Electron / Node.js
// In your Electron main process or renderer:
const crypto = require('crypto')
const os = require('os')

function deviceFingerprint() {
  // Stable identifier — e.g. hostname + first MAC
  const seed = os.hostname() + '|' + (Object.values(os.networkInterfaces())
    .flat().find((i) => i && !i.internal)?.mac || '')
  return crypto.createHash('sha256').update(seed).digest('hex')
}

const res = await fetch('https://freelancetimer-license.pages.dev/api/license/health', {
  method: 'GET',
  
})
const data = await res.json()

Error codes

All failed responses use this exact envelope. The error_code is what your desktop app should branch on; message is safe to show users directly.

{
  "success": false,
  "error_code": "ERR_INVALID_KEY",
  "message": "User-friendly message here",
  "details": "Optional technical detail for logging",
  "retry_after": 60     // only on ERR_RATE_LIMITED
}
CodeHTTPUser-facing message
ERR_INVALID_KEY404This license key is not valid. Please check the key and try again.
ERR_DEVICE_LIMIT403Maximum devices reached for this license. Please deactivate another device or request a transfer.
ERR_REVOKED403This license has been revoked. Please contact support.
ERR_EXPIRED403This license has expired. Please renew to continue using the app.
ERR_REFUNDED403This license has been refunded and is no longer valid.
ERR_OFFLINE_GRACE_EXPIRED403Please connect to the internet to verify your license. Offline grace period has ended.
ERR_RATE_LIMITED429Too many requests. Please try again later.
ERR_DEVICE_NOT_REGISTERED403This device is not registered for this license. Please activate first.
ERR_SERVER_ERROR500Server error. Please try again later.
ERR_MISSING_FIELDS400Required information is missing. Please fill all fields.
ERR_EMAIL_MISMATCH403The email address does not match the license owner on file.
ERR_PENDING_PAYMENT403Your license is awaiting payment confirmation.
ERR_INVALID_BODY400Invalid request body. Expected valid JSON.
ERR_INVALID_API_KEY401Invalid or missing API key.
ERR_GMAIL_MISMATCH403This license is bound to a different Gmail account. Please sign in with the email the license was purchased for.
ERR_GMAIL_TOKEN_INVALID401Could not verify your Google sign-in. Please try signing in with Google again.
ERR_GMAIL_REQUIRED400A verified Gmail address is required to activate this license.
ERR_TOKEN_INVALID401Activation token is invalid or has been tampered with.
ERR_TOKEN_EXPIRED401Activation token has expired. Please reactivate the device.
ERR_CHALLENGE_INVALID401Heartbeat challenge nonce is invalid, expired or already used.
ERR_CHALLENGE_PROOF401Heartbeat proof is invalid. Reactivate the device.
ERR_LICENSE_REVIEW403This license is temporarily under review for unusual activity. Contact support to restore access.

CORS

All /api/license/* endpoints accept these origins by default:

  • http://localhost:*, https://localhost:*, http(s)://127.0.0.1:* — development
  • file:// — Electron file protocol
  • null Origin — some sandboxed Electron builds
  • Any origin listed (comma-separated) in settings.app_origin

Preflight OPTIONS requests are handled automatically. Allowed headers: Content-Type, X-License-Key, X-Device-Fingerprint, X-API-Key, X-Activation-Token, X-Requested-With, Authorization.

Exposed response headers: X-Signature, X-Signing-Kid.