Client Endpoints

2.7k visitas Markdown

The nine endpoints of the client resource: read, create, update, delete, credential checks, sign-in and custom field values. 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.

The same endpoints are callable from inside WISECP

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

get/api/v1/admin/clients
Clients/GetClients admin model users::list

Lists clients with filtering and pagination. Page size caps at 100; a larger value is quietly reduced.

Query parameters 5
searchstringSearches name and email.
statusstringactive or blocked. Full list: reference/statuses?entity=client.
group_idintClient group id. List: clients/groups.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — one client
idintClient id.
full_namestringFull name.
company_namestringCompany name; empty for an individual.
emailstringEmail address.
phonestringPhone; digits only are stored.
statusstringactive or blocked.
groupobjectThe group this client belongs to.
idintGroup id.
namestringDisplay name.
languagestringLanguage code.
country_codestringISO country code, for example US.
currency_codestringCurrency code, for example USD.
email_verifiedboolWhether the email is verified.
phone_verifiedboolWhether the phone is verified.
active_servicesintNumber of services in use.
created_atstringCreation time.
last_login_atstringLast sign-in time.
Pagination meta
metaobjectIdentical on every list endpoint.
totalintTotal record count.
pageintCurrent page.
limitintPage size.
next_pageintNext page, or 0.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/clients' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Accept: application/json' \
  -d status=active \
  -d limit=25
const 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;
}
Response
{
  "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

get/api/v1/admin/clients/{id}
Clients/GetClient admin model users::get

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.

Path parameter 1
idintrequiredClient id.
Response fields 15
idintClient id.
full_namestringFull name.
namestringFirst name. The list carries only full_name.
surnamestringLast name.
statusstringactive, blocked or cancelled. The list never returns cancelled.
country_idintCountry id. Resolve with reference/countries.
currency_idintCurrency id.
group_idintClient group id. Where the list returns a group object, this returns the id only.
balancefloatAccount balance, in balance_currency_id.
balance_currency_idintCurrency the balance is held in. A funded wallet keeps its own currency, which can differ from currency_id; an empty wallet reports currency_id.
company_namestringCompany name.
emailstringEmail address.
phonestringPhone.
languagestringLanguage code.
created_atstringCreation time.
last_login_atstringLast sign-in time.
Errors 2
not_found404No such client.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/clients
Clients/CreateClient admin same handler as the panel

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.

Body 15 fields, 3 required
full_namestringrequiredFull name.
emailstringrequiredValidated for format and unique across every client in the installation.
passwordstringrequiredAt least options/password-length characters; defaults to 6.
typestringindividual or corporate. Defaults to individual.
phonestringPhone. Digits are kept, formatting is dropped.
languagestringLanguage code. Defaults to general/local.
group_idintClient group. List: clients/groups.
country_codestringISO country code, for example US. Resolve with reference/countries.
currency_codestringCurrency code, for example USD.
companyobjectTax details for a corporate client.
namestringTrading name.
tax_numberstringTax number.
tax_officestringTax office.
identitystringIdentity or tax number.
marketing_notificationsboolTurns marketing notifications on.
verify_emailboolMarks the email as verified.
verify_phoneboolMarks the phone as verified.
send_welcome_emailboolSends the welcome email.
Response fields data — 15
dataobjectThe client that was created, returned with 201. Same shape as the detail endpoint.
Errors 14
full_name_required422Full name was empty.
email_invalid422Email format is not valid.
email_exists422That email is already registered.
password_required422Password was empty.
password_too_short422The password is shorter than the installation's minimum length; details.min carries the minimum.
password_missing_characters422The password lacks a character class the installation's password policy requires.
phone_invalid422The phone number is not valid. It is checked whatever its length, so a short number such as 46 is refused too; details.field is phone.
phone_taken422The phone number is already used by another client. Checked only while phone uniqueness is on.
tax_number_invalid422For a corporate client in Türkiye, company.tax_number is not a 10-digit tax ID (VKN) or an 11-digit T.C. identity number; details.field is company.tax_number.
field_too_long422A text field is longer than its column allows (full_name 160, email 150, phone 30, company.name 200 characters). The message names the field and its limit; details.field is the request field.
currency_unavailable422currency_code names a currency that is inactive or has no exchange rate.
language_invalid422language is not an installed, active language.
create_failed500The record could not be created.
insufficient_scope403The key lacks the required scope.
Request
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'];
Response
{
  "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,
    "balance_currency_id": 2
  }
}
{
  "error": {
    "code": "email_exists",
    "message": "A client with this email already exists."
  }
}

Updating a Client

patch/api/v1/admin/clients/{id}
Clients/UpdateClient admin partial update

Changes only the fields you send and leaves the rest alone. Every body field is optional.

Body 17 fields, all optional
full_namestringFull name. If sent, it cannot be empty.
emailstringValid and unique email.
statusstringactive, blocked or cancelled.
phonestringMobile phone. Sending it empty clears it.
landline_phonestringLandline. Sending it empty clears it.
typestringindividual or corporate.
identitystringIdentity or tax number.
birthdaystringDate of birth. An empty or invalid value clears the field.
languagestringLanguage code.
group_idintClient group.
currency_codestringCurrency code, for example USD.
companyobjectCorporate details.
namestringTrading name.
tax_numberstringTax number.
tax_officestringTax office.
billing flagsbooltrue sets the flag, false clears it, leaving the field out changes nothing.
tax_exemptionboolExempts the client from tax.
never_suspendboolNever suspends the services.
never_cancelboolNever cancels the services.
separate_invoicesboolInvoices each service separately.
never_late_feeboolApplies no late fee.
Response fields data — 15
dataobjectThe client as it now stands. Same shape as the detail endpoint.
Errors 12
not_found404No such client.
full_name_required422Full name was sent empty.
email_invalid422Email format is not valid.
email_exists422That email is already in use.
phone_invalid422The phone number is not valid. It is checked whatever its length, so a short number such as 46 is refused too; details.field is phone.
phone_taken422The new phone number is already used by another client. Checked only while phone uniqueness is on; the client's own number is accepted.
landline_phone_taken422The new landline number is already used by another client. Checked only while landline uniqueness is on; the client's own number is accepted.
tax_number_invalid422For a client in Türkiye, the new company.tax_number (or a switch to type: corporate) is not a 10-digit tax ID (VKN) or an 11-digit T.C. identity number. An unchanged stored number is not checked again.
currency_unavailable422currency_code names a currency that is inactive or has no exchange rate. Sending the client's current currency again is accepted.
language_invalid422language is not an installed, active language.
field_too_long422A sent value is longer than its column allows (for example the first or last name part of full_name over 80 characters, company.name over 200). The message names the field and its limit; details.field is the request field, and nothing is written.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/clients/{id}
Clients/DeleteClient admin cannot be undone

Deletes the client and returns the id that was removed.

Response fields data
deletedboolWhether the delete succeeded.
idintId of the deleted client.
Errors 2
not_found404No such client.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/clients/validate
Clients/ValidateClient admin writes nothing

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.

The stored password never comes back

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.

Body 2 fields, both required
emailstringrequiredThe account's email address.
passwordstringrequiredThe password to check.
Response fields data — 2
validboolAlways true. A mismatch comes back as an error, not as valid: false.
user_idintThe client the credentials belong to.
Errors 3
credentials_required422email or password is missing.
invalid_credentials422No live member account matches this pair.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/clients/sso
Clients/CreateClientSsoToken admin single use, 60 seconds

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.

How the ticket behaves

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. A stored password is required. Some imported accounts have none; for such an account the ticket is spent and the sign-in form shows the reason.

Body 3 fields, 1 required
client_idintrequiredClient to issue the ticket for. user_id is still accepted as its former name.
destinationstringWhere the client lands after signing in: an absolute URL on this installation, or a route key such as services. Defaults to their dashboard.
destination_valuesarrayRoute parameters, when destination is a route key.
Response fields data — 3
tokenstringThe ticket, in the form {client_id}-{secret}.
urlstringSign-in URL carrying the ticket; send the client here.
expires_atstringExpiry, ISO 8601 with offset.
Errors 5
client_id_required422client_id is missing.
client_not_found404No client with this id.
client_not_active422The account is inactive, blocked or otherwise cannot sign in.
sso_ticket_failed422The key could not be issued.
insufficient_scope403The key lacks the required scope.
Request
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'];

Reading Custom Field Values

get/api/v1/admin/clients/{id}/custom-fields
Clients/GetClientCustomFields admin

Returns every active custom field defined for the client's language, each with the value stored for this client. When the client's language has no fields, the installation language is used; meta.language says which one was read.

Path parameters 1
idintClient id.
Response fields data[] — 8
idintField definition id (users_custom_fields.id).
namestringThe field label as the operator defined it.
typestringtext, textarea, select, radio, checkbox or date.
requiredboolThe client must fill it in.
client_editableboolfalse when the client can see the field but not change it.
client_visibleboolfalse when the field is for staff only.
optionsstring[]Choices of select, radio and checkbox fields; empty for free text.
valuestring|string[]The stored value. Checkbox fields return the picked options as an array; an empty field returns "" (or []).
Errors 2
not_found404No such client.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/clients/42/custom-fields' \
  -H "Authorization: Bearer $API_KEY"
$fields = Api::Clients()->GetClientCustomFields(['id' => 42])['data'] ?? [];
Response
{
  "data": [
    { "id": 2, "name": "How did you hear about us?", "type": "select", "required": false,
      "client_editable": true, "client_visible": true,
      "options": ["Google", "Social Media", "From a friend"], "value": "Google" }
  ],
  "meta": { "language": "en" }
}

Updating Custom Field Values

patch/api/v1/admin/clients/{id}/custom-fields
Clients/UpdateClientCustomFields admin

Writes the fields you send and leaves the rest untouched. Values go through the same normaliser as the panel: select and radio must name one of the options, checkbox takes an array (or a comma-separated string), text and date are cleaned of tags. Send "" (or []) to clear a field. The client-area flags do not bind the operator: every active field can be written. A change is logged as profile-custom-fields-updated and fires action:user.updated.

Path parameters 1
idintClient id.
Body fields 1
custom_fieldsobjectRequired. Map of field id to value; only the ids present are written.
Response fields 2
dataobject[]The full field list after the write, same items as the read call.
meta.changedstring[]The info keys actually written (field_{id}); empty when every value already matched.
Errors 7
not_found404No such client.
custom_fields_invalid422custom_fields is missing, empty or not an object.
custom_field_not_found422The id is not an active field of the client's language.
custom_field_option_invalid422The value is not one of the options; details.options lists them.
custom_field_value_invalid422The value does not fit the type: an array for a single-value field, or a date that is not YYYY-MM-DD.
custom_field_required422A required field was sent empty.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/clients/42/custom-fields' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"custom_fields": {"2": "Google", "144": "VIP client"}}'
$response = Api::Clients()->UpdateClientCustomFields([
    'id'            => 42,
    'custom_fields' => [2 => 'Google', 144 => 'VIP client'],
]);

$written = $response['meta']['changed'] ?? [];

Pitfalls

List and detail do not share a schema

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.

On update an empty value means delete

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.

A phone is not stored the way you write it

Formatting is dropped and only digits remain. Send +1 201 555 0123 and you read back 12015550123; any code that compares the two has to account for it.

¿Le ha resultado útil?

¡Gracias por sus comentarios!

¿Todavía necesita ayuda?

Nuestro equipo de soporte está disponible las 24 horas para ayudarle con lo que no encuentre aquí.