Quick Reads

2.3k visitas Markdown

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

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 or 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 }
}

Reading a Client's Account Statement

get/api/v1/admin/clients/{id}/statement
Clients/GetClientStatement admin invoice viewing privilege

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.

Query parameters 4
currencystringStatement currency, as an ISO 4217 code or a currency id. It must be one the client has movements in. Defaults to the client's currency, then its account-credit currency, when the client has movements in it; otherwise the currency with the most movements.
periodstringall, 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.
fromstringFirst day of a custom period, as Y-m-d, included. Empty leaves the start open.
tostringLast day of a custom period, as Y-m-d, included. Empty leaves the end open.
Response fields data — 12
client_idintClient id.
currencystringStatement currency, as an ISO 4217 code.
currency_idintStatement currency id.
currenciesobject[]Every currency the client has movements in. Each has its own statement.
codestringISO 4217 code.
idintCurrency id.
linesintNumber of statement lines in this currency over all time.
periodobjectThe period that was applied.
presetstringThe period value that was used.
fromstring | nullFirst day of the period (Y-m-d); null when the start is open.
tostring | nullLast day of the period (Y-m-d); null when the end is open.
openingfloatBalance before the period starts.
debitfloatTotal debit inside the period.
creditfloatTotal credit inside the period.
closingfloatBalance at the end of the period: opening + debit - credit.
closing_sidestringdebit when the client owes, credit when the client is in credit, empty when the balance is zero.
walletobjectAccount credit in this currency.
ledgerfloatAccount credit as the credit movements add up.
recordedfloatAccount credit held on the client record; 0 when the record holds its credit in another currency.
differencefloatrecorded - ledger. Not zero when the credit was changed without a movement record, for example by an import. The statement follows the movements.
linesobject[]Statement lines in date order. The first line is always the opening balance.
typestringopening, 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).
datestringDate of the movement, raw (Y-m-d H:i:s). The opening line carries the first day of the period.
invoiceobject | nullThe invoice the line belongs to: id, number as the client sees it, and status. null on the opening line and on credit movements not tied to an invoice.
methodstringPayment method module of a payment or refund line, for example PayPal, BankTransfer or Balance. Empty on other lines.
descriptionstringDescription stored with a credit movement. Empty on other lines.
top_upboolThe invoice is a credit top-up invoice.
paid_from_creditboolThe invoice was paid from account credit. That payment and the credit movement are not listed.
to_creditboolThe refund went to account credit. The refund payout and the credit movement are not listed.
unrecordedboolThe payment or refund payout has no payment record of its own and is worked out from the invoice status, as with an imported invoice or one marked paid by hand.
debitfloatDebit amount; 0 on a credit line.
creditfloatCredit amount; 0 on a debit line.
balancefloatRunning balance after this line.
Meta meta
metaobjectThe statement is not paged.
countintNumber of lines, the opening line included.
Errors 6
not_found404No such client. A staff account id gets the same answer.
currency_invalid422currency is not one the client has movements in. details.available lists the codes you can use.
period_invalid422period is not one of the five values.
date_invalid422from or to is not a valid Y-m-d date. details.field names it.
invoice_privilege_required403The key's owner lacks the invoice viewing privilege the panel tab requires.
insufficient_scope403The key lacks the required scope.
Request
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']);
}
Response
{
  "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

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.

The statement is not paged

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.

¿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í.