Quick Reads
The seven read-only endpoints that search clients, give the counts and fill the summary card. They also list a client's activity and sent messages, and build the account statement.
Overview
These seven 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. The two list endpoints return the same records as the Activity and Messages tabs on the client page. The statement endpoint returns the lines of the Account Statement tab.
Two look across the installation: search and statistics. Five look at a single client: summary, the standout note, activity, sent messages and the account statement. Whether the path carries a client id tells you which one you are on.
Reference
Searching for a Client
Searches clients by name, company or e-mail. It was written for autocomplete boxes.
search is accepted too.curl -G 'https://panel.example.com/api/v1/admin/clients/search' \
-H "Authorization: Bearer $API_KEY" \
-d q=acmeconst 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
Returns the counts across every client in the installation. It looks at no single client.
all_time, today, week, month or year. Defaults to all_time.curl -G 'https://panel.example.com/api/v1/admin/clients/stats' \
-H "Authorization: Bearer $API_KEY" \
-d period=monthconst 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']);{
"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
Returns summary card data for one client: revenue, service and ticket counts, badges, trust score.
reference/currencies.poor, fair, good or excellent.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'];{
"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
Returns one of the client's notes: the pinned one if there is one, otherwise the newest.
null when the note is not pinned, or was pinned before this field existed.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
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.
1.1 to 100. Defaults to 25; a larger value is capped at 100.Y-m-d, or a range written as two dates joined by a spaced hyphen: 2026-10-01 - 2026-10-12.date is compared: eq, neq, gte, gt, lte or lt. Defaults to eq: on that day or inside that range.0 for the system.client (the client or one of its users), staff, system, or deleted when the acting account no longer exists.in, alteration, login, added or delete.Y-m-d H:i:s).0.date is neither Y-m-d nor a Y-m-d - Y-m-d range.date_op is not one of the six operators.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;{
"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
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.
1.1 to 100. Defaults to 25; a larger value is capped at 100.Y-m-d, or a range written as two dates joined by a spaced hyphen: 2026-10-01 - 2026-10-12.date is compared: eq, neq, gte, gt, lte or lt. Defaults to eq: on that day or inside that range.type.email or sms.two-factor-verification, subuser-invitation or message-from-admin; empty when the send stored none.Y-m-d H:i:s).0.date is neither Y-m-d nor a Y-m-d - Y-m-d range.date_op is not one of the six operators.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;{
"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 }
}Reading a Client's Account Statement
Returns one client's account statement in one currency. It lists the opening balance, every invoice, payment, refund and credit movement with a running balance, and the closing balance. The Account Statement tab and its PDF and CSV exports use the same engine. Not available in 5.0-beta.3 and earlier.
The balance is debit minus credit. A positive figure means the client owes that amount. A negative one means the client is in credit, for example with unspent account credit. A cancelled invoice adds no debit line, but a payment made to it is still listed. An invoice paid from account credit lists neither that payment nor the credit movement, since they cancel out. Its invoice line carries paid_from_credit instead. The key's owner also needs the invoice viewing privilege, the same one the panel tab asks for.
all, this-year, last-year, last-12 or custom. Defaults to last-12, the last 12 months, as in the panel; becomes custom when you send from or to without a period.custom period, as Y-m-d, included. Empty leaves the start open.custom period, as Y-m-d, included. Empty leaves the end open.Y-m-d); null when the start is open.Y-m-d); null when the end is open.opening + debit - credit.debit when the client owes, credit when the client is in credit, empty when the balance is zero.0 when the record holds its credit in another currency.recorded - ledger. Not zero when the credit was changed without a movement record, for example by an import. The statement follows the movements.opening, invoice (debit), payment (credit), refund (credit: the invoice was refunded), refund_out (debit: the refund paid back to the client), credit_in (credit added to the account) or credit_out (account credit used).Y-m-d H:i:s). The opening line carries the first day of the period.id, number as the client sees it, and status. null on the opening line and on credit movements not tied to an invoice.PayPal, BankTransfer or Balance. Empty on other lines.0 on a credit line.0 on a debit line.currency is not one the client has movements in. details.available lists the codes you can use.period is not one of the five values.from or to is not a valid Y-m-d date. details.field names it.curl -G 'https://panel.example.com/api/v1/admin/clients/42/statement' \
--data-urlencode 'currency=USD' \
--data-urlencode 'period=this-year' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/clients/42/statement?currency=USD&period=last-year', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
// Positive means the client owes, negative means the client is in credit.
const owes = body.data.closing > 0;$query = http_build_query(['currency' => 'USD', 'period' => 'custom', 'from' => '2026-01-01', 'to' => '2026-06-30']);
$ch = curl_init('https://panel.example.com/api/v1/admin/clients/42/statement?' . $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()->GetClientStatement(['id' => 42], ['period' => 'all']);
// One statement per currency: walk the currencies the first answer lists.
foreach ($response['data']['currencies'] ?? [] as $currency) {
$statement = Api::Clients()->GetClientStatement(['id' => 42], ['currency' => $currency['code'], 'period' => 'all']);
}{
"data": {
"client_id": 42,
"currency": "USD",
"currency_id": 4,
"currencies": [ { "code": "USD", "id": 4, "lines": 162 }, { "code": "EUR", "id": 5, "lines": 4 } ],
"period": { "preset": "custom", "from": "2026-10-01", "to": "2026-10-10" },
"opening": -332.37,
"debit": 0,
"credit": 53.88,
"closing": -386.25,
"closing_side": "credit",
"wallet": { "ledger": 392.25, "recorded": 392.25, "difference": 0 },
"lines": [
{
"type": "opening", "date": "2026-10-01 00:00:00", "invoice": null, "method": "", "description": "",
"top_up": false, "paid_from_credit": false, "to_credit": false, "unrecorded": false,
"debit": 0, "credit": 0, "balance": -332.37
},
{
"type": "payment", "date": "2026-10-05 09:20:07",
"invoice": { "id": 2886, "number": "#2886", "status": "paid" },
"method": "PayPal", "description": "", "top_up": false, "paid_from_credit": false,
"to_credit": false, "unrecorded": false, "debit": 0, "credit": 47.88, "balance": -380.25
},
{
"type": "payment", "date": "2026-10-06 09:20:07",
"invoice": { "id": 2887, "number": "#2887", "status": "paid" },
"method": "PayPal", "description": "", "top_up": false, "paid_from_credit": false,
"to_credit": false, "unrecorded": false, "debit": 0, "credit": 6, "balance": -386.25
}
]
},
"meta": { "count": 3 }
}Pitfalls
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.
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 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 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.
The statement endpoint returns every line of the period in one response, because a running balance cannot be split across pages. Narrow it with period, or with from and to. Each currency is a separate statement: a client with invoices in two currencies needs two calls. currencies tells you which ones exist.
Related Articles
¡Gracias por sus comentarios!
Nuestro equipo de soporte está disponible las 24 horas para ayudarle con lo que no encuentre aquí.