Licenses
License API
Manage license validation, activation, and device tracking.
429 Too Many Requests with a Retry-After: 60 header until the window resets.Validate License
Validate and activate a license key. This endpoint is rate-limited to 5 requests per minute per IP address.
Endpoint: POST /api/licenses/validate
| Parameter | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | License key to validate |
device_id | string | No | Unique device identifier for activation tracking |
curl -X POST "$BASE_URL/api/licenses/validate" \
-H "Content-Type: application/json" \
-d '{"license_key":"XXXX-XXXX-XXXX-XXXX","device_id":"device_abc123"}'
interface LicenseValidation {
valid: boolean
license: {
id: string
status: string
product: object
activation_date: string
expiration_date: string
current_activations: number
}
}
const result: LicenseValidation = await $fetch('/api/licenses/validate', {
method: 'POST',
body: {
license_key: 'XXXX-XXXX-XXXX-XXXX',
device_id: 'device_abc123'
}
})
import requests
response = requests.post(
"$BASE_URL/api/licenses/validate",
json={
"license_key": "XXXX-XXXX-XXXX-XXXX",
"device_id": "device_abc123"
}
)
result = response.json()
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func validateLicense() {
payload := map[string]string{
"license_key": "XXXX-XXXX-XXXX-XXXX",
"device_id": "device_abc123",
}
body, _ := json.Marshal(payload)
resp, _ := http.Post(
"$BASE_URL/api/licenses/validate",
"application/json",
bytes.NewBuffer(body),
)
defer resp.Body.Close()
}
<?php
$response = file_get_contents(
'$BASE_URL/api/licenses/validate',
false,
stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
'license_key' => 'XXXX-XXXX-XXXX-XXXX',
'device_id' => 'device_abc123'
])
]
])
);
$result = json_decode($response, true);
use reqwest::Client;
use serde_json::json;
let client = Client::new();
let response = client
.post("$BASE_URL/api/licenses/validate")
.json(&json!({
"license_key": "XXXX-XXXX-XXXX-XXXX",
"device_id": "device_abc123"
}))
.send()
.await?;
Response (Success):
{
"valid": true,
"license": {
"id": "uuid",
"status": "active",
"product": { "id": "uuid", "name": "PRO", "features": [] },
"activation_date": "2024-01-01T00:00:00Z",
"expiration_date": "2025-12-31T23:59:59Z",
"current_activations": 1
}
}
Response (Error):
{
"statusCode": 400,
"statusMessage": "Invalid license key or validation failed"
}
Deactivate License
Authentication RequiredRequires the session_token session cookie of the license owner (see Authentication), you can only deactivate a license you own.
Deactivate a license from a specific device or all devices.
Endpoint: POST /api/licenses/deactivate
| Parameter | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | License key to deactivate |
device_id | string | No | Specific device to deactivate (omit for all devices) |
curl -X POST "$BASE_URL/api/licenses/deactivate" \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"license_key":"XXXX-XXXX-XXXX-XXXX","device_id":"device_abc123"}'
const result = await $fetch('/api/licenses/deactivate', {
method: 'POST',
// session cookie (session_token) is sent automatically same-origin
body: {
license_key: 'XXXX-XXXX-XXXX-XXXX',
device_id: 'device_abc123'
}
})
import requests
response = requests.post(
"$BASE_URL/api/licenses/deactivate",
headers={"Cookie": f"session_token={token}"},
json={
"license_key": "XXXX-XXXX-XXXX-XXXX",
"device_id": "device_abc123"
}
)
req, _ := http.NewRequest("POST",
"$BASE_URL/api/licenses/deactivate",
bytes.NewBuffer(body))
req.Header.Set("Cookie", "session_token="+token)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
<?php
$response = file_get_contents(
'$BASE_URL/api/licenses/deactivate',
false,
stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n" .
"Cookie: session_token=$token",
'content' => json_encode([
'license_key' => 'XXXX-XXXX-XXXX-XXXX',
'device_id' => 'device_abc123'
])
]
])
);
let response = client
.post("$BASE_URL/api/licenses/deactivate")
.header("Cookie", format!("session_token={}", token))
.json(&json!({
"license_key": "XXXX-XXXX-XXXX-XXXX",
"device_id": "device_abc123"
}))
.send()
.await?;
Response:
{
"success": true,
"message": "Device deactivated successfully",
"remaining_activations": 4
}
License Status
Authentication RequiredRequires the session_token session cookie of the license owner (see Authentication), you can only inspect a license you own.
Check the current status of a license. Rate-limited to 30 requests per minute per IP.
Endpoint: GET /api/licenses/status
| Parameter | Type | Required | Description |
|---|---|---|---|
license_key | query | Yes | License key to check |
curl -X GET "$BASE_URL/api/licenses/status?license_key=XXXX-XXXX-XXXX-XXXX" \
-b cookies.txt
const status = await $fetch('/api/licenses/status', {
// session cookie (session_token) is sent automatically same-origin
query: { license_key: 'XXXX-XXXX-XXXX-XXXX' }
})
import requests
response = requests.get(
"$BASE_URL/api/licenses/status",
headers={"Cookie": f"session_token={token}"},
params={"license_key": "XXXX-XXXX-XXXX-XXXX"}
)
req, _ := http.NewRequest("GET",
"$BASE_URL/api/licenses/status?license_key=XXXX-XXXX-XXXX-XXXX",
nil)
req.Header.Set("Cookie", "session_token="+token)
resp, _ := http.DefaultClient.Do(req)
<?php
$response = file_get_contents(
'$BASE_URL/api/licenses/status?license_key=XXXX-XXXX-XXXX-XXXX',
false,
stream_context_create([
'http' => [
'header' => "Cookie: session_token=$token"
]
])
);
let response = client
.get("$BASE_URL/api/licenses/status")
.header("Cookie", format!("session_token={}", token))
.query(&[("license_key", "XXXX-XXXX-XXXX-XXXX")])
.send()
.await?;
Response:
{
"license": {
"id": "uuid",
"license_key": "XXXX-XXXX-XXXX-XXXX",
"status": "active",
"activation_date": "2024-01-01T00:00:00Z",
"expiration_date": "2025-12-31T23:59:59Z",
"days_until_expiration": 365,
"activation_limit": 5,
"current_activations": 2,
"available_activations": 3,
"is_expired": false,
"is_active": true,
"product": { "id": "uuid", "name": "PRO" },
"user": { "id": "uuid", "email": "user@example.com" }
},
"active_devices": [
{
"id": "uuid",
"device_id": "device_abc123",
"activated_at": "2024-01-01T00:00:00Z",
"ip_address": "192.168.1.1"
}
]
}
License Lifecycle
Rate Limiting
License endpoints are rate-limited per IP over a fixed 60-second window. On a 429, one header is returned:
| Header | Description |
|---|---|
Retry-After | Seconds until the window resets (always 60) |
Rate Limits by Endpoint
| Endpoint | Requests / 60s | Tier |
|---|---|---|
/api/licenses/validate | 5 | Strict |
/api/licenses/status | 30 | Standard |
/api/licenses/deactivate | 30 | Standard |
POST /api/licenses/validate applies the Strict limit twice: once per IP, and once keyed on the submitted license_key, so a single key cannot be hammered from many IPs.
429 Too Many Requests for the remainder of the 60-second window. There is no progressive or escalating block duration.License Types
| Type | Description | Features |
|---|---|---|
TRIAL | Legacy v1 desktop trial (10-minute session). v2 uses the free account plan instead. | Limited to single room |
PRO | Professional license | Unlimited rooms, custom branding |
ENTERPRISE | Enterprise license | All features, priority support, SSO |