Service Metrics
The five endpoints that configure billing by usage and read the measurements and period records.
Overview
Metered billing charges by what is used rather than a flat fee: disk, traffic, calls. These five endpoints configure what gets measured, read the measurements, and show the period records that were raised.
What can be measured is decided by the module; how much is free, what the tiers cost and where the ceiling sits are yours to write per service. The list endpoint returns both together, so one request shows which metrics a service could turn on.
Reference
Listing the Metrics
Returns what the module can measure and what is configured on the service, in one list.
per_unit prices every unit the same, volume prices the whole total at the tier it lands in, graduated adds up each tier at its own price.curl 'https://panel.example.com/api/v1/admin/services/529/metrics' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/metrics', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/metrics');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The list also carries what COULD be configured; separate the live ones with 'configured'.
$metrics = Api::Services()->GetServiceMetrics(['id' => 529])['data'];
$live = array_filter($metrics, fn (array $m): bool => $m['configured'] && $m['enabled']);{
"data": [
{
"key": "disk_space",
"label": "Disk Space",
"unit": "GB",
"supported": true,
"configured": true,
"enabled": true,
"included": 10,
"max_value": 100,
"scheme": "per_unit",
"pricing": {
"1-100": {
"from": 1,
"to": 100,
"USD": { "enable": 1, "price": 0.5 }
}
}
},
{
"key": "bandwidth",
"label": "Bandwidth",
"unit": "GB",
"supported": true,
"configured": false,
"enabled": false,
"included": 0,
"max_value": 0,
"scheme": "per_unit",
"pricing": []
}
]
}Configuring a Metric
Writes one metric's free allowance, its ceiling and its price tiers.
per_unit prices every unit the same, volume prices the whole total at the tier it lands in, graduated adds up each tier at its own price. Defaults to per_unit.{enable, price} object. Tiers whose upper bound is zero or below the lower one are ignored.curl -X PUT 'https://panel.example.com/api/v1/admin/services/529/metrics/disk_space' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"enabled":true,"included":10,"max_value":100,"scheme":"per_unit","pricing":[{"from":1,"to":100,"USD":{"enable":1,"price":0.5}}]}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space', {
method: 'PUT',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: true,
included: 10,
max_value: 100,
scheme: 'per_unit',
pricing: [{ from: 1, to: 100, USD: { enable: 1, price: 0.5 } }],
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'enabled' => true,
'included' => 10,
'scheme' => 'per_unit',
'pricing' => [
['from' => 1, 'to' => 100, 'USD' => ['enable' => 1, 'price' => 0.5]],
],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Tiers go in as an ARRAY and come back as an OBJECT: do not send back what you read unchanged.
$current = Api::Services()->GetServiceMetrics(['id' => 529])['data'][0];
$tiers = array_values($current['pricing']);
Api::Services()->UpdateServiceMetric([
'id' => 529,
'metric' => 'disk_space',
'pricing' => $tiers,
]);{
"error": {
"code": "currency_required",
"message": "Enabling a metric requires at least one currency with a price."
}
}Removing a Metric
Takes the metric out of the service configuration. The usage and billing records stay put.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/529/metrics/disk_space' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space');
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);// Removing does NOT delete the history: the billing rows stay queryable.
$response = Api::Services()->DeleteServiceMetric([
'id' => 529,
'metric' => 'disk_space',
]);Reading the Usage
Returns the daily usage points for the period you give.
curl -G 'https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/usage' \
-H "Authorization: Bearer $API_KEY" \
-d period_start=2026-06-01 \
-d period_end=2026-06-30const url = new URL('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/usage');
url.searchParams.set('period_start', '2026-06-01');
url.searchParams.set('period_end', '2026-06-30');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();$url = 'https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/usage?' . http_build_query([
'period_start' => '2026-06-01',
'period_end' => '2026-06-30',
]);
$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);$usage = Api::Services()->GetServiceMetricUsage(
['id' => 529, 'metric' => 'disk_space'],
['period_start' => '2026-06-01', 'period_end' => '2026-06-30'],
);
// An empty value means 'no reading that day', NOT 'zero usage that day'.
$measured = array_filter($usage['data']['points'], fn (array $p): bool => $p['value'] !== null);Reading the Billing History
Returns the period records raised for the metric.
curl 'https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/billing' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/billing', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/metrics/disk_space/billing');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$rows = Api::Services()->GetServiceMetricBilling([
'id' => 529,
'metric' => 'disk_space',
])['data'];
// Rows with no invoice behind them have not been charged yet.
$pending = array_filter($rows, fn (array $r): bool => $r['invoice_id'] === 0);Pitfalls
You send the price tiers as an array, but the response returns them as an object keyed by their bounds. Sending back what you read does not work; you have to turn the values into a plain array first.
When a day's value comes back empty, no reading was taken that day; it does not mean usage was zero. An average that treats empties as zeros shows days when measurement stopped as days when usage dropped.
The remove endpoint only takes the metric out of the configuration. The usage points and billing rows stay where they are and remain queryable, so nothing already charged disappears. Configure the metric again and the old history is still beside it.
Turning a metric on for billing needs at least one active currency priced in the tiers, or the request answers currency_required. You do not have to turn it on to set the allowance: included can be written on a metric that stays off.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.