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/activateActivate 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
| Code | Error | Cause |
|---|---|---|
| 400 | Missing required fields | licenseKey, machineId, or productSlug is missing |
| 404 | Invalid license key | License key does not exist |
| 403 | License is suspended/revoked | License status is not ACTIVE |
| 403 | License has expired | License expiration date has passed |
| 403 | License is not valid for this product | Product slug does not match the license |
| 403 | Maximum machines reached | All 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/validateValidate 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
| Code | Error | Extra Fields |
|---|---|---|
| 400 | Missing required fields | — |
| 404 | Invalid license key | — |
| 403 | License is suspended/revoked | — |
| 403 | License has expired | — |
| 403 | License is not valid for this product | — |
| 403 | Machine not activated | canActivate: true if slots available |
| 403 | Maximum 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/deactivateDeactivate 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
| Code | Error | Cause |
|---|---|---|
| 400 | Missing required fields | licenseKey or machineId is missing |
| 404 | Invalid license key | License key does not exist |
| 404 | Machine is not activated | No 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());
}