Licences
Manage licence validation, activation, and device tracking.
429 Too Many Requests with a Retry-After: 60 header until the window resets.Validate Licence
Validate and activate a licence 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 | Licence key to validate |
device_id | string | Yes | Stable, unique identifier for the device being activated. Send the same value every time for a given device, it is what claims and re-claims that device's seat |
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": "studio", "features": ["client_api", "api_keys"] },
"activation_date": "2024-01-01T00:00:00Z",
"expiration_date": "2025-12-31T23:59:59Z",
"current_activations": 1
}
}
Response (Error):
{
"statusCode": 400,
"statusMessage": "Invalid licence key or validation failed"
}
| Status | When |
|---|---|
400 | license_key or device_id is missing, empty or over-long, or the key is unknown, inactive or expired |
409 | Every seat on the licence is already held by another device, and none was idle long enough to reclaim |
429 | Rate limit exceeded, see Rate Limiting |
400 therefore never says which field was at fault, confirm both license_key and device_id are present and non-empty before retrying.Deactivate Licence
Authentication RequiredRequires the session_token session cookie of the licence owner (see Authentication), you can only deactivate a licence you own.
Deactivate a licence from a specific device or all devices.
Endpoint: POST /api/licenses/deactivate
| Parameter | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | Licence 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",
"deactivated_count": 1,
"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 activation_limit - remaining_activations, using activation_limit from GET /api/licenses/status. Without device_id the message reads "All devices deactivated successfully"; deactivated_count is the number of seats this call released.Licence Status
Authentication RequiredRequires the session_token session cookie of the licence owner (see Authentication), you can only inspect a licence you own.
Check the current status of a licence. Rate-limited to 30 requests per minute per IP.
Endpoint: GET /api/licenses/status
| Parameter | Type | Required | Description |
|---|---|---|---|
license_key | query | Yes | Licence 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",
"purchase_date": "2024-01-01T00:00:00Z",
"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": "Ultra", "description": "...", "features": [] },
"user": { "id": "uuid", "first_name": "Jane", "last_name": "Doe", "email": "user@example.com" }
},
"active_devices": [
{
"id": "uuid",
"device_id": "device_abc123",
"device_name": "Studio Mac",
"platform": "macos",
"app_version": "3.1.2",
"activated_at": "2024-01-01T00:00:00Z",
"last_seen_at": "2024-01-02T09:00:00Z",
"ip_address": "192.168.1.1"
}
]
}
Licence Lifecycle
Rate Limiting
Licence 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.Licence 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 | A simple clicker: one room, one presenter, watermarked screen sharing and Stream Deck; every other feature is locked |
presenter | Presenters | Timer Pro, chat, drawing, multi-host and NDI |
pro (Pro) | Solo producers | Callers, custom rooms, video sharing and the Studio essentials |
ultra (Max) | Power users and streamers | Unlimited rooms, saved brand colour themes, layers, ISO recording, replay and the wider Studio toolset |
studio (Ultra) | Production teams | Room layout designer, whitelabel branding, the Client API, API keys, custom domains, pooled per-seat team licences |
enterprise | Large orgs and procurement | SSO, SCIM and custom team roles, per-seat annual contracts |