许可证
许可证验证、激活与设备管理 API
管理许可证验证、激活与设备跟踪。
速率限制:许可证端点按 IP 在固定的 60 秒窗口内进行速率限制。超出限制后将返回
429 Too Many Requests 及 Retry-After: 60 响应头,直至窗口重置。验证许可证
验证并激活许可证密钥。此端点按每个 IP 地址限制为每分钟 5 次请求。
端点: POST /api/licenses/validate
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
license_key | string | 是 | 要验证的许可证密钥 |
device_id | string | 是 | 待激活设备的稳定且唯一的标识符。同一设备每次都必须发送相同的值,该设备的席位正是依据此值来占用和重新占用的 |
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?;
响应(成功):
{
"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
}
}
响应(错误):
{
"statusCode": 400,
"statusMessage": "Invalid licence key or validation failed"
}
| 状态码 | 触发条件 |
|---|---|
400 | license_key 或 device_id 缺失、为空或过长,或者密钥未知、未激活或已过期 |
409 | 该许可证的所有席位均已被其他设备占用,且没有任何席位闲置足够长的时间可供回收 |
429 | 超出速率限制,请参阅速率限制 |
出于安全考虑,所有许可证验证错误均返回相同的通用消息,以防范枚举攻击。因此,
400 错误绝不会指明是哪个字段出错,重试前请确认 license_key 和 device_id 均已提供且不为空。停用许可证
需要身份验证需要许可证所有者的 session_token 会话 Cookie(请参阅身份验证),您只能停用自己拥有的许可证。
在指定设备或所有设备上停用许可证。
端点: POST /api/licenses/deactivate
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
license_key | string | 是 | 要停用的许可证密钥 |
device_id | string | 否 | 要停用的特定设备(省略则停用所有设备) |
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?;
响应:
{
"success": true,
"message": "Device deactivated successfully",
"deactivated_count": 1,
"remaining_activations": 4
}
尽管名称如此,
remaining_activations 表示停用后仍在使用的席位数,即该许可证上仍处于激活状态的不同设备数量。它并不是空闲席位数。因此,停用所有设备时将返回 0。空闲席位数的计算方式为 activation_limit - remaining_activations,其中 activation_limit 取自 GET /api/licenses/status。未提供 device_id 时,消息为“All devices deactivated successfully”;deactivated_count 为本次调用释放的席位数。许可证状态
需要身份验证需要许可证所有者的 session_token 会话 Cookie(请参阅身份验证),您只能查看自己拥有的许可证。
查询许可证的当前状态。按每个 IP 限制为每分钟 30 次请求。
端点: GET /api/licenses/status
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
license_key | query | 是 | 要查询的许可证密钥 |
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?;
响应:
{
"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": "Studio 8K", "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"
}
]
}
许可证生命周期
速率限制
许可证端点按 IP 在固定的 60 秒窗口内进行速率限制。返回 429 时,会附带一个响应头:
| 响应头 | 说明 |
|---|---|
Retry-After | 距窗口重置的秒数(始终为 60) |
各端点的速率限制
| 端点 | 请求数 / 60 秒 | 级别 |
|---|---|---|
/api/licenses/validate | 5 | 严格 |
/api/licenses/status | 30 | 标准 |
/api/licenses/deactivate | 30 | 标准 |
POST /api/licenses/validate 会两次应用严格级别限制:一次按 IP 计算,另一次按提交的 license_key 计算,因此无法从多个 IP 对同一个密钥发起密集请求。
超出限制后,在 60 秒窗口的剩余时间内将一律返回
429 Too Many Requests。不存在渐进式或逐步延长的封禁时长。许可证类型
每个许可证都对应当前方案级别之一。各级别的功能与限制以定价页面所展示的内容为准,请参阅比较方案。
| 级别 | 适用对象 | 相较下一级别的主要新增功能 |
|---|---|---|
free | 永久免费方案,无需绑卡,永不过期 | 简单的演示遥控器:一个房间、一位演讲者、带水印的屏幕共享和 Stream Deck;其他所有功能均锁定 |
presenter | 演讲者 | Timer Pro、聊天、绘图、多主持人和 NDI |
pro(Studio HD) | 个人制作者 | 连线嘉宾、自定义房间、视频共享以及 Studio 核心功能 |
ultra(Studio 4K) | 高级用户和主播 | 无限房间、已保存的品牌配色主题、图层、ISO 录制、回放以及更丰富的 Studio 工具集 |
studio(Studio 8K) | 制作团队 | 房间布局设计器、白标品牌、Client API、API 密钥、自定义域名、按席位计算的共享团队许可证池 |
enterprise | 大型组织与采购部门 | SSO、SCIM 和自定义团队角色、按席位签订的年度合同 |
较早的许可证可能仍沿用旧版产品名称。这些许可证保留购买时所含的权益,API 会将其映射到上述级别。