Client Endpoints
The seven endpoints of the client resource: read, create, update, delete, plus credential checks and sign-in. Each one lists the fields you send, the fields you get back and a working sample.
Overview
The client resource is the API side of the panel's Clients screen. It applies the same business rules. The password length and the email uniqueness you meet here are the ones the panel enforces, because the endpoint reuses the handler the panel calls.
All seven endpoints belong to the admin audience and expect the key to carry the matching scope. Scope names are written on each endpoint's identity line below.
Writing a module or an addon? You do not have to go out over HTTP. Api::Clients()->GetClients() runs the same endpoint in process and returns the same envelope. The first tab of every sample shows that call.
Reference
Listing Clients
Lists clients with filtering and pagination. Page size caps at 100; a larger value is quietly reduced.
active or blocked. Full list: reference/statuses?entity=client.clients/groups.active or blocked.US.USD.0.curl -G 'https://panel.example.com/api/v1/admin/clients' \
-H "Authorization: Bearer $API_KEY" \
-H 'Accept: application/json' \
-d status=active \
-d limit=25const url = new URL('https://panel.example.com/api/v1/admin/clients');
url.searchParams.set('status', 'active');
url.searchParams.set('limit', '25');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
for (const c of body.data) console.log(c.id, c.full_name);$url = 'https://panel.example.com/api/v1/admin/clients?' . http_build_query([
'status' => 'active',
'limit' => 25,
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json',
],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($body['data'] as $client) {
echo $client['id'], ' ', $client['full_name'], PHP_EOL;
}// From inside WISECP: no HTTP, same envelope.
$response = Api::Clients()->GetClients([], [
'status' => 'active',
'limit' => 25,
]);
if (isset($response['error'])) {
Logger::error($response['error']['message']);
return;
}
foreach ($response['data'] as $client) {
echo $client['id'], ' ', $client['full_name'], PHP_EOL;
}{
"data": [
{
"id": 42,
"full_name": "John Doe",
"company_name": "",
"email": "[email protected]",
"phone": "5550100",
"status": "active",
"group": { "id": 1, "name": "Standard" },
"email_verified": true,
"phone_verified": false,
"active_services": 3,
"created_at": "2026-01-01 10:00:00",
"last_login_at": "2026-06-20 09:00:00"
}
],
"meta": { "total": 128, "page": 1, "limit": 25, "next_page": 2 }
}{
"error": {
"code": "insufficient_scope",
"message": "API key lacks the required scope."
}
}Client Detail
Returns one client's full profile. Fields the list does not carry arrive here: first and last name separately, the balance, and the country and currency ids.
full_name.active, blocked or cancelled. The list never returns cancelled.reference/countries.group object, this returns the id only.curl 'https://panel.example.com/api/v1/admin/clients/42' \
-H "Authorization: Bearer $API_KEY" \
-H 'Accept: application/json'const res = await fetch('https://panel.example.com/api/v1/admin/clients/42', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Clients()->GetClient(['id' => 42]);
if (isset($response['error'])) {
// not_found or insufficient_scope
return false;
}
$client = $response['data'];
echo $client['name'], ' ', $client['surname'], PHP_EOL;Creating a Client
Creates a client and returns the new record in the detail schema. Business rules run in the panel's own handler, so a setting such as password length applies here too.
options/password-length characters; defaults to 6.individual or corporate. Defaults to individual.general/local.clients/groups.US. Resolve with reference/countries.USD.201. Same shape as the detail endpoint.curl -X POST 'https://panel.example.com/api/v1/admin/clients' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"full_name":"John Doe","email":"[email protected]","password":"Str0ngP@ssw0rd"}'const res = await fetch('https://panel.example.com/api/v1/admin/clients', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
full_name: 'John Doe',
email: '[email protected]',
password: 'Str0ngP@ssw0rd',
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/clients');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'full_name' => 'John Doe',
'email' => '[email protected]',
'password' => 'Str0ngP@ssw0rd',
]),
]);
$created = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Clients()->CreateClient([
'full_name' => 'John Doe',
'email' => '[email protected]',
'password' => 'Str0ngP@ssw0rd',
'type' => 'individual',
'country_code' => 'US',
'currency_code' => 'USD',
]);
if (isset($response['error'])) {
// email_exists is the one you will meet most often.
throw new Exception($response['error']['message']);
}
$clientId = $response['data']['id'];{
"data": {
"id": 43,
"full_name": "John Doe",
"name": "John",
"surname": "Doe",
"email": "[email protected]",
"status": "active",
"country_id": 840,
"currency_id": 2,
"group_id": 0,
"balance": 0.00
}
}{
"error": {
"code": "email_exists",
"message": "A client with this email already exists."
}
}Updating a Client
Changes only the fields you send and leaves the rest alone. Every body field is optional.
active, blocked or cancelled.individual or corporate.USD.true sets the flag, false clears it, leaving the field out changes nothing.curl -X PATCH 'https://panel.example.com/api/v1/admin/clients/42' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"status":"blocked","never_suspend":false}'const res = await fetch('https://panel.example.com/api/v1/admin/clients/42', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: 'blocked', never_suspend: false }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'status' => 'blocked',
'never_suspend' => false,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Only two fields are sent; the rest of the profile stays as it was.
$response = Api::Clients()->UpdateClient([
'id' => 42,
'status' => 'blocked',
'never_suspend' => false,
]);Deleting a Client
Deletes the client and returns the id that was removed.
curl -X DELETE 'https://panel.example.com/api/v1/admin/clients/42' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/clients/42', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Clients()->DeleteClient(['id' => 42]);
if (($response['data']['deleted'] ?? false) === true) {
// The record is gone; clear your own data that pointed at it.
}Validating Client Credentials
Answers one question: do this email and password belong to a live member account? Nothing is created or changed. Use it when your own front end holds the credentials and needs the client id behind them.
It is a bcrypt digest wrapped in the installation's own encryption. Returning it would let any holder of an admin key verify guesses offline. The response carries the client id and nothing else.
true. A mismatch comes back as an error, not as valid: false.email or password is missing.curl -X POST 'https://panel.example.com/api/v1/admin/clients/validate' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"correct horse battery staple"}'const res = await fetch('https://panel.example.com/api/v1/admin/clients/validate', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: '[email protected]', password }),
});
if (res.ok) {
const body = await res.json();
console.log(body.data.user_id); // the client behind the credentials
}$ch = curl_init('https://panel.example.com/api/v1/admin/clients/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'email' => '[email protected]',
'password' => $password,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Clients()->ValidateClient([
'email' => '[email protected]',
'password' => $password,
]);
// A wrong pair arrives as an error, so reaching this line already means they matched.
$clientId = $response['data']['user_id'] ?? 0;Signing In as a Client
Issues a one-time sign-in ticket for a client and returns the URL that spends it. Send the client to that URL and they arrive already signed in. Your integration never handles their password.
Intended for a system that already knows who the visitor is: your own portal, a control panel, a desk tool. login_as_client is not exposed over the API because it swaps the PHP session in place; this endpoint is its token-based equivalent.
Single use — it is spent the moment the URL is opened. A replayed link lands on the sign-in form with an explanatory message. Valid for 60 seconds, meant to be issued and followed in one motion. One per client: a new ticket silently retires the previous one. Confined to this installation, so a destination pointing anywhere else is dropped and the client lands on their dashboard. The login gate still applies. Account status, country blocking and module vetoes are evaluated when the ticket is spent, exactly as for a password login. The ticket proves who, not whether they may sign in right now.
user_id is still accepted as its former name.services. Defaults to their dashboard.destination is a route key.{client_id}-{secret}.client_id is missing.curl -X POST 'https://panel.example.com/api/v1/admin/clients/sso' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"client_id":42,"destination":"services","destination_values":[128]}'const res = await fetch('https://panel.example.com/api/v1/admin/clients/sso', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ client_id: 42, destination: 'services', destination_values: [128] }),
});
const body = await res.json();
window.location = body.data.url; // spend it now; it lasts 60 seconds$ch = curl_init('https://panel.example.com/api/v1/admin/clients/sso');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => 42,
'destination' => 'services',
'destination_values' => [128],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Clients()->CreateClientSsoToken(['client_id' => 42]);
$link = $response['data']['url'];Pitfalls
The list returns the group as an object (group). The detail returns the id alone (group_id). The name is one field in the list and two in the detail. A mapper written against the list quietly produces empty fields when it meets the detail.
Sending an empty phone or birthday clears the field. To keep a field, leave it out of the body entirely; that is what a partial update is for.
Formatting is dropped and only digits remain. Send +1 555 010 0000 and you read back 15550100000; any code that compares the two has to account for it.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.