License API

The License API allows client applications to activate machines, validate license keys, and deactivate machines. No authentication is required — these endpoints are designed to be called directly from your desktop or mobile application.

POST/api/license/activate

Activate a machine on a license key. If the machine is already activated, this endpoint updates the last-seen timestamp and returns success.

Request Body

{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "machineId": "unique-machine-identifier",
  "productSlug": "nexadeck",
  "machineName": "Chris's MacBook Pro",  // optional
  "platform": "macos"                     // optional
}

TypeScript Interface

interface ActivateRequest {
  licenseKey: string;   // Required. The license key to activate.
  machineId: string;    // Required. Unique identifier for this machine.
  productSlug: string;  // Required. Product slug (e.g. "nexadeck").
  machineName?: string; // Optional. Human-readable machine name.
  platform?: string;    // Optional. Operating system (e.g. "macos", "windows").
}

Success Response (200)

{
  "success": true,
  "message": "Machine activated",
  "activation": {
    "machineId": "unique-machine-identifier",
    "machineName": "Chris's MacBook Pro",
    "activatedAt": "2025-01-15T10:30:00.000Z"
  },
  "activeMachines": 1,
  "maxMachines": 3
}

Already Activated Response (200)

{
  "success": true,
  "message": "Machine already activated",
  "activation": {
    "machineId": "unique-machine-identifier",
    "machineName": "Chris's MacBook Pro",
    "activatedAt": "2025-01-15T10:30:00.000Z"
  }
}

Error Responses

CodeErrorCause
400Missing required fieldslicenseKey, machineId, or productSlug is missing
404Invalid license keyLicense key does not exist
403License is suspended/revokedLicense status is not ACTIVE
403License has expiredLicense expiration date has passed
403License is not valid for this productProduct slug does not match the license
403Maximum machines reachedAll machine slots are in use

Example (curl)

curl -X POST https://your-domain.com/api/license/activate \
  -H "Content-Type: application/json" \
  -d '{
    "licenseKey": "XXXX-XXXX-XXXX-XXXX",
    "machineId": "machine-unique-id",
    "productSlug": "nexadeck",
    "machineName": "My Laptop",
    "platform": "windows"
  }'

Example (TypeScript)

const response = await fetch("https://your-domain.com/api/license/activate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    licenseKey: "XXXX-XXXX-XXXX-XXXX",
    machineId: getMachineId(),
    productSlug: "nexadeck",
    machineName: os.hostname(),
    platform: process.platform,
  }),
});

const data = await response.json();

if (data.success) {
  console.log("Activated:", data.activation.machineId);
} else {
  console.error("Activation failed:", data.error);
}

Example (Rust)

use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct ActivateResponse {
    success: bool,
    message: Option<String>,
    error: Option<String>,
}

let client = Client::new();
let res = client
    .post("https://your-domain.com/api/license/activate")
    .json(&serde_json::json!({
        "licenseKey": "XXXX-XXXX-XXXX-XXXX",
        "machineId": machine_id,
        "productSlug": "nexadeck",
        "machineName": hostname,
        "platform": "macos"
    }))
    .send()
    .await?;

let data: ActivateResponse = res.json().await?;
if data.success {
    println!("Activated successfully");
} else {
    eprintln!("Activation failed: {}", data.error.unwrap_or_default());
}
POST/api/license/validate

Validate that a license key is active and the machine is currently activated. Also updates the last-seen timestamp for the machine.

Request Body

{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "machineId": "unique-machine-identifier",
  "productSlug": "nexadeck"
}

TypeScript Interface

interface ValidateRequest {
  licenseKey: string;   // Required. The license key to validate.
  machineId: string;    // Required. Machine identifier to check.
  productSlug: string;  // Required. Product slug (e.g. "nexadeck").
}

Success Response (200)

{
  "valid": true,
  "edition": "pro",
  "editionName": "Professional",
  "features": ["feature-a", "feature-b", "feature-c"],
  "expiresAt": "2026-01-15T00:00:00.000Z",
  "maxMachines": 3,
  "activeMachines": 1
}

Error Responses

CodeErrorExtra Fields
400Missing required fields
404Invalid license key
403License is suspended/revoked
403License has expired
403License is not valid for this product
403Machine not activatedcanActivate: true if slots available
403Maximum machines reached

Example (curl)

curl -X POST https://your-domain.com/api/license/validate \
  -H "Content-Type: application/json" \
  -d '{
    "licenseKey": "XXXX-XXXX-XXXX-XXXX",
    "machineId": "machine-unique-id",
    "productSlug": "nexadeck"
  }'

Example (TypeScript)

const response = await fetch("https://your-domain.com/api/license/validate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    licenseKey: "XXXX-XXXX-XXXX-XXXX",
    machineId: getMachineId(),
    productSlug: "nexadeck",
  }),
});

const data = await response.json();

if (data.valid) {
  console.log("License valid. Edition:", data.editionName);
  console.log("Features:", data.features);
} else if (data.canActivate) {
  // Machine not yet activated — call /api/license/activate
} else {
  console.error("Validation failed:", data.error);
}

Example (Rust)

use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct ValidateResponse {
    valid: bool,
    edition: Option<String>,
    edition_name: Option<String>,
    features: Option<Vec<String>>,
    expires_at: Option<String>,
    error: Option<String>,
    can_activate: Option<bool>,
}

let client = Client::new();
let res = client
    .post("https://your-domain.com/api/license/validate")
    .json(&serde_json::json!({
        "licenseKey": "XXXX-XXXX-XXXX-XXXX",
        "machineId": machine_id,
        "productSlug": "nexadeck"
    }))
    .send()
    .await?;

let data: ValidateResponse = res.json().await?;
if data.valid {
    println!("Edition: {}", data.edition_name.unwrap_or_default());
    if let Some(features) = &data.features {
        println!("Features: {:?}", features);
    }
} else if data.can_activate.unwrap_or(false) {
    // Machine not yet activated — call /api/license/activate
} else {
    eprintln!("Validation failed: {}", data.error.unwrap_or_default());
}
POST/api/license/deactivate

Deactivate a machine from a license, freeing up a machine slot.

Request Body

{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "machineId": "unique-machine-identifier"
}

TypeScript Interface

interface DeactivateRequest {
  licenseKey: string;  // Required. The license key.
  machineId: string;   // Required. Machine identifier to deactivate.
}

Success Response (200)

{
  "success": true,
  "message": "Machine deactivated",
  "machineId": "unique-machine-identifier"
}

Error Responses

CodeErrorCause
400Missing required fieldslicenseKey or machineId is missing
404Invalid license keyLicense key does not exist
404Machine is not activatedNo active activation for this machine

Example (curl)

curl -X POST https://your-domain.com/api/license/deactivate \
  -H "Content-Type: application/json" \
  -d '{
    "licenseKey": "XXXX-XXXX-XXXX-XXXX",
    "machineId": "machine-unique-id"
  }'

Example (TypeScript)

const response = await fetch("https://your-domain.com/api/license/deactivate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    licenseKey: "XXXX-XXXX-XXXX-XXXX",
    machineId: getMachineId(),
  }),
});

const data = await response.json();

if (data.success) {
  console.log("Machine deactivated:", data.machineId);
} else {
  console.error("Deactivation failed:", data.error);
}

Example (Rust)

use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct DeactivateResponse {
    success: bool,
    message: Option<String>,
    machine_id: Option<String>,
    error: Option<String>,
}

let client = Client::new();
let res = client
    .post("https://your-domain.com/api/license/deactivate")
    .json(&serde_json::json!({
        "licenseKey": "XXXX-XXXX-XXXX-XXXX",
        "machineId": machine_id
    }))
    .send()
    .await?;

let data: DeactivateResponse = res.json().await?;
if data.success {
    println!("Machine deactivated");
} else {
    eprintln!("Deactivation failed: {}", data.error.unwrap_or_default());
}