API Reference
Licenses
Issue keys, read them, freeze them, move their clock and delete them.
A license is one key on one application. It carries a duration, a device limit, and a clock that only starts when the key is first used.
Keys issued through this API are free of charge and may be lifetime, because the token acts as the workspace owner rather than as a reseller spending a balance.
Worth knowing
/licenses/7K2M-9QXD-4TPL-B8NR.
unused has never been redeemed, active is running, expired ran out, frozen has a paused clock, disabled was revoked.
#List licenses
https://api.ctyauth.com/manage/v1/licenses
Newest first. Combine the filters freely.
Query parameters
application_idstringoptional- Only keys on this application. The
pub_application id from the panel. statusstringoptional- One of
active,unused,expired,frozen,disabled. qstringoptional- Matches part of the key. Up to 64 characters.
per_pageintegeroptional- Between 1 and 200. Defaults to 50.
Can answer
/licenses
GET /manage/v1/licenses HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
curl https://api.ctyauth.com/manage/v1/licenses \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses', {
method: 'GET',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
],
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.get(
"https://api.ctyauth.com/manage/v1/licenses",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"licenses": [
{
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "active",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
],
"page": {
"current": 1,
"last": 18,
"total": 873,
"per_page": 50
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "application_unknown",
"message": "No application with that id."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Issue keys
https://api.ctyauth.com/manage/v1/licenses
Creates one or many keys at once and answers with the plain key strings.
Body
application_idstringrequired- The application to issue on. The
pub_application id from the panel. durationstringrequired- A duration the application prices, or
lifetime. quantityintegeroptional- Between 1 and 500. Defaults to 1.
max_devicesintegeroptional- Between 1 and 100. Defaults to 1.
custom_keystringoptional- Your own key string instead of a generated one. 6 to 64 characters of
A-Z a-z 0-9 . _ -, and only withquantity1.
Can answer
/licenses
POST /manage/v1/licenses HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
Content-Type: application/json
{
"application_id": "pub_your_application_id",
"duration": "7day",
"quantity": 10,
"max_devices": 2
}
curl https://api.ctyauth.com/manage/v1/licenses \
-X POST \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret" \
-H "Content-Type: application/json" \
-d '{"application_id":"pub_your_application_id","duration":"7day","quantity":10,"max_devices":2}'
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses', {
method: 'POST',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ application_id: 'pub_your_application_id', duration: '7day', quantity: 10, max_devices: 2 }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['application_id' => 'pub_your_application_id', 'duration' => '7day', 'quantity' => 10, 'max_devices' => 2]),
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.post(
"https://api.ctyauth.com/manage/v1/licenses",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
json={"application_id": "pub_your_application_id", "duration": "7day", "quantity": 10, "max_devices": 2},
)
response.raise_for_status()
data = response.json()
{
"keys": [
"7K2M-9QXD-4TPL-B8NR",
"3B8N-2PLV-6HQW-T1ZC"
],
"licenses": [
{
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "active",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
]
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "plan_limit",
"message": "Your plan limit has been reached.",
"limit": {
"key": "licenses",
"max": 5000,
"used": 5000
}
}
{
"error": "token_scope",
"message": "This token is locked to one application. Use a workspace token for this endpoint."
}
{
"error": "not_allowed",
"message": "This application is not accepting new keys."
}
{
"error": "application_unknown",
"message": "No application with that id."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Read one key
https://api.ctyauth.com/manage/v1/licenses/{key}
The key with every device currently bound to it, most recently seen first.
Path parameters
keystringrequired- The license key.
Can answer
/licenses/{key}
GET /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR', {
method: 'GET',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
],
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.get(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"license": {
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "active",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
},
"devices": [
{
"hwid": "a1b2c3d4e5f6",
"last_ip": "203.0.113.9",
"last_version": "1.4.2",
"first_seen_at": "2026-09-12T10:00:00+00:00",
"last_seen_at": "2026-09-19T08:41:00+00:00"
}
]
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}
#Update a key
https://api.ctyauth.com/manage/v1/licenses/{key}
Revoke or restore a key, or change how many devices it allows.
Path parameters
keystringrequired- The license key.
Body
is_activebooleanoptional- False revokes the key, true restores it.
max_devicesintegeroptional- Between 1 and 100. Lowering it does not unbind devices already on the key.
Can answer
/licenses/{key}
PATCH /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
Content-Type: application/json
{
"is_active": false
}
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR \
-X PATCH \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret" \
-H "Content-Type: application/json" \
-d '{"is_active":false}'
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR', {
method: 'PATCH',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ is_active: false }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['is_active' => false]),
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.patch(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
json={"is_active": False},
)
response.raise_for_status()
data = response.json()
{
"license": {
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "disabled",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Freeze and unfreeze
https://api.ctyauth.com/manage/v1/licenses/{key}/freeze
Freezing pauses the clock and keeps the time the key has left. POST /licenses/{key}/unfreeze starts it again from that point. Either way the time spent frozen is added back to the expiry date.
Path parameters
keystringrequired- The license key.
Body
resume_on_loginbooleanoptional- True lets the key unfreeze itself the next time a client signs in with it. False, the default, keeps it frozen until you unfreeze it. Freezing a key that is already frozen only updates this choice.
Can answer
/licenses/{key}/freeze
POST /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/freeze HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
Content-Type: application/json
{
"resume_on_login": true
}
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/freeze \
-X POST \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret" \
-H "Content-Type: application/json" \
-d '{"resume_on_login":true}'
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/freeze', {
method: 'POST',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ resume_on_login: true }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/freeze');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['resume_on_login' => true]),
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.post(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/freeze",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
json={"resume_on_login": True},
)
response.raise_for_status()
data = response.json()
{
"license": {
"status": "frozen",
"resume_on_login": true,
"key": "7K2M-9QXD-4TPL-B8NR",
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Reset devices
https://api.ctyauth.com/manage/v1/licenses/{key}/reset-devices
Unbinds every device on the key so the next machine to run it takes a free slot. The clock is untouched.
Path parameters
keystringrequired- The license key.
Can answer
/licenses/{key}/reset-devices
POST /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/reset-devices HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/reset-devices \
-X POST \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/reset-devices', {
method: 'POST',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/reset-devices');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
],
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.post(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/reset-devices",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"license": {
"devices": {
"used": 0,
"max": 3
},
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "active",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"first_used_at": "2026-09-12T10:00:00+00:00",
"expires_at": "2026-09-19T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}
#Extend and shorten
https://api.ctyauth.com/manage/v1/licenses/{key}/extend
Adds time to a running key, priced as one duration times a count. POST /licenses/{key}/shorten takes the same body and gives time back without refunding anything.
Path parameters
keystringrequired- The license key.
Body
durationstringrequired- A duration the application prices.
countintegeroptional- How many of that duration. Between 1 and 50. Defaults to 1.
Can answer
/licenses/{key}/extend
POST /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/extend HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
Content-Type: application/json
{
"duration": "7day",
"count": 2
}
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/extend \
-X POST \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret" \
-H "Content-Type: application/json" \
-d '{"duration":"7day","count":2}'
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/extend', {
method: 'POST',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ duration: '7day', count: 2 }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/extend');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['duration' => '7day', 'count' => 2]),
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.post(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR/extend",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
json={"duration": "7day", "count": 2},
)
response.raise_for_status()
data = response.json()
{
"license": {
"expires_at": "2026-10-03T10:00:00+00:00",
"key": "7K2M-9QXD-4TPL-B8NR",
"status": "active",
"resume_on_login": false,
"duration": "7day",
"application_id": "pub_your_application_id",
"application": "My App",
"devices": {
"used": 1,
"max": 3
},
"first_used_at": "2026-09-12T10:00:00+00:00",
"created_at": "2026-09-11T18:22:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}
{
"error": "license_not_movable",
"message": "That key cannot take a time change. Lifetime keys, keys that have never been used and durations this application does not price are skipped."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Delete a key
https://api.ctyauth.com/manage/v1/licenses/{key}
Removes the key and its devices for good. Revoking with is_active: false is usually what you want instead, because it keeps the record.
Path parameters
keystringrequired- The license key.
Can answer
/licenses/{key}
DELETE /manage/v1/licenses/7K2M-9QXD-4TPL-B8NR HTTP/1.1
Host: api.ctyauth.com
X-Api-Key: ck_your_key
X-Api-Secret: sk_your_secret
Accept: application/json
curl https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR \
-X DELETE \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR', {
method: 'DELETE',
headers: {
'X-Api-Key': process.env.API_KEY,
'X-Api-Secret': process.env.API_SECRET,
'Accept': 'application/json',
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('API_KEY'),
'X-Api-Secret: ' . getenv('API_SECRET'),
'Accept: application/json',
],
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.delete(
"https://api.ctyauth.com/manage/v1/licenses/7K2M-9QXD-4TPL-B8NR",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"deleted": "7K2M-9QXD-4TPL-B8NR"
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "license_unknown",
"message": "No license with that key."
}