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
}
remaining_activations is the number of seats still in use after the deactivation, that is, the count of distinct devices left active on the licence. It is not the number of free seats. Deactivating all devices therefore returns 0. Compute free seats as times_activated_max - remaining_activations using the max from GET /api/licenses/status.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
A licence resolves to one of the current plan tiers. The authoritative feature and limit set for each tier is what the pricing page renders, see Compare plans.
| Tier | Who it is for | Notable additions over the tier below |
|---|---|---|
free | Permanent free plan, no card, no expiry | Core toolkit with a watermark, one room, one remote client |
pro | Solo presenters | Watermark removed, multi-host, higher caller and source caps |
ultra | Power users and streamers | Client API, room layout designer, saved brand colour themes |
studio | Production teams | Whitelabel branding, API keys, custom domains, pooled team seats |
enterprise | Large orgs and procurement | SSO, SCIM and custom team roles, per-seat annual contracts |