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 functionalitywarning — license valid but expires soon; show a bannerread_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 IPActivate 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 IPVerify 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 IPGet 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 IPGet 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 fingerprintPeriodic 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-keyunrestrictedGet 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 IPRelease 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 IPRequest 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/healthunrestrictedHealth 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
}| Code | HTTP | User-facing message |
|---|
| ERR_INVALID_KEY | 404 | This license key is not valid. Please check the key and try again. |
| ERR_DEVICE_LIMIT | 403 | Maximum devices reached for this license. Please deactivate another device or request a transfer. |
| ERR_REVOKED | 403 | This license has been revoked. Please contact support. |
| ERR_EXPIRED | 403 | This license has expired. Please renew to continue using the app. |
| ERR_REFUNDED | 403 | This license has been refunded and is no longer valid. |
| ERR_OFFLINE_GRACE_EXPIRED | 403 | Please connect to the internet to verify your license. Offline grace period has ended. |
| ERR_RATE_LIMITED | 429 | Too many requests. Please try again later. |
| ERR_DEVICE_NOT_REGISTERED | 403 | This device is not registered for this license. Please activate first. |
| ERR_SERVER_ERROR | 500 | Server error. Please try again later. |
| ERR_MISSING_FIELDS | 400 | Required information is missing. Please fill all fields. |
| ERR_EMAIL_MISMATCH | 403 | The email address does not match the license owner on file. |
| ERR_PENDING_PAYMENT | 403 | Your license is awaiting payment confirmation. |
| ERR_INVALID_BODY | 400 | Invalid request body. Expected valid JSON. |
| ERR_INVALID_API_KEY | 401 | Invalid or missing API key. |
| ERR_GMAIL_MISMATCH | 403 | This license is bound to a different Gmail account. Please sign in with the email the license was purchased for. |
| ERR_GMAIL_TOKEN_INVALID | 401 | Could not verify your Google sign-in. Please try signing in with Google again. |
| ERR_GMAIL_REQUIRED | 400 | A verified Gmail address is required to activate this license. |
| ERR_TOKEN_INVALID | 401 | Activation token is invalid or has been tampered with. |
| ERR_TOKEN_EXPIRED | 401 | Activation token has expired. Please reactivate the device. |
| ERR_CHALLENGE_INVALID | 401 | Heartbeat challenge nonce is invalid, expired or already used. |
| ERR_CHALLENGE_PROOF | 401 | Heartbeat proof is invalid. Reactivate the device. |
| ERR_LICENSE_REVIEW | 403 | This 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:* — developmentfile:// — Electron file protocolnull 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.