Quick Reads

1.6k vues Markdown

The six read-only endpoints that search clients, give the counts, fill the summary card and list a client's activity and sent messages.

Overview

These six endpoints tell you something quickly about a client and change nothing. The panel's search box, dashboard counters and summary card all read from them, and the two list endpoints return the same records as the Activity and Messages tabs on the client page.

Two look across the installation (search and statistics) and four at a single client (summary, the standout note, activity and sent messages). Whether the path carries a client id tells you which one you are on.

Reference

get/api/v1/admin/clients/search
Clients/SearchClients admin autocomplete

Searches clients by name, company or e-mail. It was written for autocomplete boxes.

Query parameters 1
qstringThe search term. The name search is accepted too.
Response fields data[] — 4
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
textstringA ready-made display label with the name and company joined.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/clients/search' \
  -H "Authorization: Bearer $API_KEY" \
  -d q=acme
const url = new URL('https://panel.example.com/api/v1/admin/clients/search');
url.searchParams.set('q', 'acme');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/clients/search?' . http_build_query(['q' => 'acme']);

$ch = curl_init($url);
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()->SearchClients([], ['q' => 'acme']);

Client Statistics

get/api/v1/admin/clients/stats
Clients/GetClientsStats admin all clients

Returns the counts across every client in the installation. It looks at no single client.

Query parameters 1
periodstringThe period: all_time, today, week, month or year. Defaults to all_time.
Response fields data — 9
activeintHow many clients are active.
blockedintHow many clients are blocked.
newintHow many clients arrived in the period.
blacklistedintHow many clients are blacklisted.
active_servicesintHow many services are live.
unpaid_invoicesintHow many invoices are unpaid.
credit_balancefloatThe total credit balance. A raw number; the currency symbol is yours to add.
growth_ratefloatThe growth rate as a percentage.
totalintThe total client count in the period.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/clients/stats' \
  -H "Authorization: Bearer $API_KEY" \
  -d period=month
const url = new URL('https://panel.example.com/api/v1/admin/clients/stats');
url.searchParams.set('period', 'month');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/clients/stats?' . http_build_query(['period' => 'month']);

$ch = curl_init($url);
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()->GetClientsStats([], ['period' => 'month']);
Response
{
  "data": {
    "active": 120,
    "blocked": 3,
    "new": 18,
    "blacklisted": 2,
    "active_services": 340,
    "unpaid_invoices": 41,
    "credit_balance": 14620.12,
    "growth_rate": 4.5,
    "total": 145
  }
}

Client Summary

get/api/v1/admin/clients/{id}/summary
Clients/GetClientSummary admin badges and trust score

Returns summary card data for one client: revenue, service and ticket counts, badges, trust score.

Response fields data — 13
user_idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
created_atstringWhen they joined. A raw value; formatting is yours.
total_revenuefloatWhat they have paid in total so far.
revenue_currencyintCurrency id of the amount. List: reference/currencies.
paid_invoicesintHow many invoices were paid.
active_servicesintHow many services are live.
inactive_servicesintHow many services are not live.
total_ticketsintHow many tickets in total.
recent_ticketsintHow many tickets recently.
badgesobjectWhich badges were earned.
loyalboolThe membership age threshold was passed.
revenueboolThe revenue threshold was passed.
multi_serviceboolThey hold more than one live service.
experiencedboolThe past interaction threshold was passed.
trust_scoreobjectThe trust score and its parts.
totalintThe total score. Between 0 and 100.
labelstringpoor, fair, good ya da excellent.
servicesintPoints from the service axis.
revenueintPoints from the revenue axis.
ageintPoints from the account age axis.
ticketsintPoints from the support ticket axis.
Errors 2
not_found404No such client.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/clients/42/summary' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/clients/42/summary', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42/summary');
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()->GetClientSummary(['id' => 42]);

// The parts tell you where the score came from.
$score = $response['data']['trust_score'];
$fromServices = $score['services'];
Response
{
  "data": {
    "user_id": 9,
    "full_name": "Test Client",
    "company_name": "",
    "created_at": "2021-03-18 00:00:00",
    "total_revenue": 36,
    "revenue_currency": 4,
    "paid_invoices": 1,
    "active_services": 16,
    "inactive_services": 16,
    "total_tickets": 0,
    "recent_tickets": 0,
    "badges": {
      "loyal": true,
      "revenue": false,
      "multi_service": true,
      "experienced": true
    },
    "trust_score": {
      "total": 50,
      "label": "fair",
      "services": 30,
      "revenue": 5,
      "age": 15,
      "tickets": 0
    }
  }
}

Reading the Standout Note

get/api/v1/admin/clients/{id}/latest-note
Clients/GetClientLatestNote admin pinned first

Returns one of the client's notes: the pinned one if there is one, otherwise the newest.

Response fields data — 9
idintNote id.
contentstringThe note content.
pinnedboolWhether the note is pinned.
added_byintId of the admin who added it.
added_by_namestringName of the admin who added it; empty for a note carried over from 3.x plain text (added_by is 0).
created_atstringWhen the note was added.
pinned_byint|nullId of the admin who pinned it. null when the note is not pinned, or was pinned before this field existed.
pinned_by_namestring|nullName of whoever pinned it — the pinned banner shows this, not the author.
pinned_atstring|nullWhen it was pinned. Editing a note that stays pinned keeps the stamp; unpinning clears all three.
Errors 2
not_found404No such client.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/clients/42/latest-note' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/clients/42/latest-note', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();

// With no notes at all, data comes back null - not a 404.
if (body.data === null) return;
$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42/latest-note');
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()->GetClientLatestNote(['id' => 42]);

// On a client with no notes, 'data' is null.
$note = $response['data'] ?? null;

Listing a Client's Activity

get/api/v1/admin/clients/{id}/activity
Clients/GetClientActivity admin paginated

Lists one client's activity records, newest first: what the client did and what staff or the system did on the account. These are the records of the Activity tab on the client page, with the same search and date filters.

Query parameters 5
pageintPage number. Defaults to 1.
limitintPage size, from 1 to 100. Defaults to 25; a larger value is capped at 100.
searchstringMatches the record category, the description (as stored and in the language of the request), the data, the actor's name or the IP address.
datestringRecord date as Y-m-d, or a range written as two dates joined by a spaced hyphen: 2026-10-01 - 2026-10-12.
date_opstringHow date is compared: eq, neq, gte, gt, lte or lt. Defaults to eq: on that day or inside that range.
Response fields data[] — 10
idintRecord id.
actor_idintAccount that performed the action; 0 for the system.
actor_typestringclient (the client or one of its users), staff, system, or deleted when the acting account no longer exists.
actor_namestringName of the acting account; empty for the system or a deleted account.
reasonstringRecord category, for example in, alteration, login, added or delete.
actionstringAction key of the record.
descriptionstringReadable description in the language of the request.
dataobject|nullDetails stored with the record. Values of sensitive keys, such as passwords and tokens, come back masked.
ipstringIP address the action came from.
created_atstringRecord date, raw (Y-m-d H:i:s).
Pagination meta
metaobjectCounts are for the filters you sent.
totalintTotal record count.
pageintCurrent page.
limitintPage size.
next_pageintNext page, or 0.
Errors 4
not_found404No such client. A staff account id gets the same answer.
invalid_date422date is neither Y-m-d nor a Y-m-d - Y-m-d range.
invalid_date_op422date_op is not one of the six operators.
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/clients/42/activity' \
  --data-urlencode 'search=login' \
  --data-urlencode 'date=2026-10-01 - 2026-10-12' \
  -H "Authorization: Bearer $API_KEY"
const query = new URLSearchParams({ search: 'login', limit: '50' });
const res   = await fetch(`https://panel.example.com/api/v1/admin/clients/42/activity?${query}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();

// meta.next_page is 0 on the last page.
const more = body.meta.next_page !== 0;
$query = http_build_query(['date' => '2026-10-01', 'date_op' => 'gte', 'limit' => 50]);
$ch    = curl_init('https://panel.example.com/api/v1/admin/clients/42/activity?' . $query);
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()->GetClientActivity(['id' => 42], ['search' => 'login', 'limit' => 50]);

foreach ($response['data'] ?? [] as $row)
    echo $row['created_at'], ' ', $row['description'], PHP_EOL;
Response
{
  "data": [
    {
      "id": 183379,
      "actor_id": 1,
      "actor_type": "staff",
      "actor_name": "Admin",
      "reason": "login",
      "action": "admin-login-as-client",
      "description": "Logged in as client John Doe (#116)",
      "data": { "user_id": 116, "user_name": "John Doe" },
      "ip": "203.0.113.10",
      "created_at": "2026-10-12 05:41:07"
    }
  ],
  "meta": { "total": 716, "page": 1, "limit": 25, "next_page": 2 }
}

Listing the Messages Sent to a Client

get/api/v1/admin/clients/{id}/messages
Clients/GetClientMessages admin e-mail and SMS

Lists the e-mails and SMS messages sent to one client, newest first, with the same search and date filters as the Messages tab on the client page. The list carries no message body.

Query parameters 5
pageintPage number. Defaults to 1.
limitintPage size, from 1 to 100. Defaults to 25; a larger value is capped at 100.
searchstringMatches the e-mail subject or SMS title, or the recipient.
datestringSend date as Y-m-d, or a range written as two dates joined by a spaced hyphen: 2026-10-01 - 2026-10-12.
date_opstringHow date is compared: eq, neq, gte, gt, lte or lt. Defaults to eq: on that day or inside that range.
Response fields data[] — 7
idintLog record id. It is unique only together with type.
typestringemail or sms.
subjectstringE-mail subject or SMS title.
recipientstringRecipient addresses (e-mail) or numbers (SMS), as stored.
reasonstringNotification reason key, for example two-factor-verification, subuser-invitation or message-from-admin; empty when the send stored none.
ipstringIP address recorded with the send.
created_atstringSend date, raw (Y-m-d H:i:s).
Pagination meta
metaobjectCounts are for the filters you sent.
totalintTotal message count.
pageintCurrent page.
limitintPage size.
next_pageintNext page, or 0.
Errors 4
not_found404No such client. A staff account id gets the same answer.
invalid_date422date is neither Y-m-d nor a Y-m-d - Y-m-d range.
invalid_date_op422date_op is not one of the six operators.
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/clients/42/messages' \
  --data-urlencode 'date=2026-10-01' \
  --data-urlencode 'date_op=gte' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/clients/42/messages?search=invoice', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();

// The list has no body: keep type + id to read one message later.
const refs = body.data.map(m => ({ type: m.type, id: m.id }));
$query = http_build_query(['search' => 'invoice', 'page' => 2]);
$ch    = curl_init('https://panel.example.com/api/v1/admin/clients/42/messages?' . $query);
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()->GetClientMessages(['id' => 42], ['date' => '2026-10-01 - 2026-10-12']);

// Read one body through the preview endpoint, with type + id from the list.
$first   = $response['data'][0] ?? null;
$preview = $first ? Api::Clients()->GetMessagePreview([], ['type' => $first['type'], 'id' => $first['id']]) : null;
Response
{
  "data": [
    {
      "id": 17313,
      "type": "email",
      "subject": "Support ticket created",
      "recipient": "[email protected]",
      "reason": "",
      "ip": "203.0.113.10",
      "created_at": "2026-10-11 18:02:44"
    },
    {
      "id": 905,
      "type": "sms",
      "subject": "Your service is ready",
      "recipient": "+15550100",
      "reason": "message-from-admin",
      "ip": "203.0.113.10",
      "created_at": "2026-10-10 09:00:12"
    }
  ],
  "meta": { "total": 522, "page": 1, "limit": 25, "next_page": 2 }
}

Pitfalls

Amounts arrive raw, not formatted

The credit balance and revenue fields are numbers without a symbol, and the date is raw too. You read the currency from its own field and do the formatting yourself. That is deliberate: the server does not know the reader's locale.

A client with no notes returns null

On a client with no notes the standout-note endpoint returns data: null, not a 404. Code that reaches straight into the fields breaks here, so check for empty first.

The trust score depends on your settings

The score in the summary is not a fixed scale; it is worked out from the tiers in your configuration. Change the tiers and the same client scores differently, so do not compare the score across installations.

The message list has no body

The sent-messages endpoint returns only the subject, recipient and date. To read a message, pass its type and id to the message preview endpoint; an id alone is not enough, because e-mails and SMS messages are numbered separately. The preview withholds the body of security and confidential messages.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.