API Reference
Members and balances
Read your resellers and distributors, move their credit, and audit every movement.
Members are the people selling under you. A reseller spends a balance to issue keys; a distributor can fund resellers of their own; a partner helps you run the workspace; a developer issues keys for free and works on your applications and resources.
This is where you wire a payment provider in: take money on your own site, then credit the member here.
Worth knowing
Workspace tokens only
Balances belong to the workspace rather than to one application, so a locked token is refused here with
token_scope.
Staff hold no balance
Owners and partners issue keys for free, so moving credit on them answers
member_is_staff. Developers issue for free too and answer member_is_developer.
Every move is recorded
Credits, debits and corrections all land in the ledger with the balance that resulted, so the history reconciles on its own.
#List members
GET
https://api.ctyauth.com/manage/v1/members
Everyone in the workspace with their current balance.
Needs a workspace token. A token locked to one application is refused with
token_scope.
Query parameters
rolestringoptionalpartner,distributor,resellerordeveloper.per_pageintegeroptional- Between 1 and 200. Defaults to 50.
Can answer
GET
/members
GET /manage/v1/members 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/members \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/members', {
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/members');
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/members",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"members": [
{
"id": 42,
"name": "Example Reseller",
"email": "[email protected]",
"role": "reseller",
"parent_id": 17,
"balance_cents": 12500,
"pending_cents": 0,
"is_active": true,
"joined_at": "2026-08-02T14:10:00+00:00"
}
],
"page": {
"current": 1,
"last": 1,
"total": 6,
"per_page": 50
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "token_scope",
"message": "This token is locked to one application. Use a workspace token for this endpoint."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Read one member
GET
https://api.ctyauth.com/manage/v1/members/{id}
A single member by id.
Needs a workspace token. A token locked to one application is refused with
token_scope.
Path parameters
idintegerrequired- The member id.
Can answer
GET
/members/{id}
GET /manage/v1/members/42 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/members/42 \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/members/42', {
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/members/42');
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/members/42",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"member": {
"id": 42,
"name": "Example Reseller",
"email": "[email protected]",
"role": "reseller",
"parent_id": 17,
"balance_cents": 12500,
"pending_cents": 0,
"is_active": true,
"joined_at": "2026-08-02T14:10:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "token_scope",
"message": "This token is locked to one application. Use a workspace token for this endpoint."
}
{
"error": "member_unknown",
"message": "No member with that id."
}
#Move a balance
POST
https://api.ctyauth.com/manage/v1/members/{id}/balance
credit adds to the balance, debit takes from it, and set forces it to the figure you name.
Needs a workspace token. A token locked to one application is refused with
token_scope.
Path parameters
idintegerrequired- The member id.
Body
actionstringrequiredcredit,debitorset.amount_centsintegerrequired- Whole cents, at least 0 and at most 100000.
notestringoptional- Lands on the ledger entry. Up to 200 characters.
Can answer
POST
/members/{id}/balance
POST /manage/v1/members/42/balance 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
{
"action": "credit",
"amount_cents": 5000,
"note": "bank transfer"
}
curl https://api.ctyauth.com/manage/v1/members/42/balance \
-X POST \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret" \
-H "Content-Type: application/json" \
-d '{"action":"credit","amount_cents":5000,"note":"bank transfer"}'
const response = await fetch('https://api.ctyauth.com/manage/v1/members/42/balance', {
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({ action: 'credit', amount_cents: 5000, note: 'bank transfer' }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.error);
$handle = curl_init('https://api.ctyauth.com/manage/v1/members/42/balance');
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(['action' => 'credit', 'amount_cents' => 5000, 'note' => 'bank transfer']),
]);
$data = json_decode(curl_exec($handle), true);
import os, requests
response = requests.post(
"https://api.ctyauth.com/manage/v1/members/42/balance",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
json={"action": "credit", "amount_cents": 5000, "note": "bank transfer"},
)
response.raise_for_status()
data = response.json()
{
"member": {
"balance_cents": 17500,
"id": 42,
"name": "Example Reseller",
"email": "[email protected]",
"role": "reseller",
"parent_id": 17,
"pending_cents": 0,
"is_active": true,
"joined_at": "2026-08-02T14:10:00+00:00"
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "token_scope",
"message": "This token is locked to one application. Use a workspace token for this endpoint."
}
{
"error": "member_unknown",
"message": "No member with that id."
}
{
"error": "member_is_staff",
"message": "Owners and partners do not hold a balance."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}
#Read the ledger
GET
https://api.ctyauth.com/manage/v1/members/{id}/ledger
Every movement on that member, newest first, each with the balance it left behind.
Needs a workspace token. A token locked to one application is refused with
token_scope.
Path parameters
idintegerrequired- The member id.
Query parameters
typestringoptional- Only entries of this kind, for example
topuporpurchase. per_pageintegeroptional- Between 1 and 200. Defaults to 50.
Can answer
GET
/members/{id}/ledger
GET /manage/v1/members/42/ledger 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/members/42/ledger \
-H "X-Api-Key: ck_your_key" \
-H "X-Api-Secret: sk_your_secret"
const response = await fetch('https://api.ctyauth.com/manage/v1/members/42/ledger', {
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/members/42/ledger');
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/members/42/ledger",
headers={
"X-Api-Key": os.environ["API_KEY"],
"X-Api-Secret": os.environ["API_SECRET"],
},
)
response.raise_for_status()
data = response.json()
{
"entries": [
{
"id": 981,
"type": "topup",
"amount_cents": 5000,
"balance_cents": 17500,
"pending_cents": 0,
"meta": {
"note": "bank transfer"
},
"created_at": "2026-09-19T09:00:00+00:00"
}
],
"page": {
"current": 1,
"last": 4,
"total": 172,
"per_page": 50
}
}
{
"error": "missing_credentials",
"message": "Send your API key and secret."
}
{
"error": "bad_credentials",
"message": "These API credentials are not valid."
}
{
"error": "token_scope",
"message": "This token is locked to one application. Use a workspace token for this endpoint."
}
{
"error": "member_unknown",
"message": "No member with that id."
}
{
"error": "validation_failed",
"message": "The duration field is required.",
"errors": {
"duration": [
"The duration field is required."
]
}
}