Service Metrics

8 views Markdown

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

get/api/v1/admin/services/{id}/metrics
Services/GetServiceMetrics admin a merged list

Returns what the module can measure and what is configured on the service, in one list.

Response fields data[] — 10
keystringThe metric key.
labelstringIts display label.
unitstringThe unit it is measured in.
supportedboolWhether the module can measure it.
configuredboolWhether it is configured on the service. Rows where this is false only show what is possible.
enabledboolWhether usage is billed.
includedfloatHow much is free before billing starts.
max_valueintThe ceiling applied in the panel.
schemestringFiyatlama şeması: 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.
pricingobjectThe tiers. An empty array on a metric that is not configured.
Errors 2
not_found404No such service.
insufficient_scope403The key lacks the required scope.
Request
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']);
Response
{
  "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

put/api/v1/admin/services/{id}/metrics/{metric}
Services/UpdateServiceMetric admin tells the module

Writes one metric's free allowance, its ceiling and its price tiers.

Body 5
enabledboolTurns billing of the usage on or off. Turning it on needs at least one active currency with a price.
includedfloatHow much is free before billing starts.
max_valueintThe ceiling applied in the panel. Sending zero leaves the current one alone.
schemestringThe pricing scheme: 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.
pricingobject[]The price tiers. Each carries a lower bound, an upper bound and, per currency code, an {enable, price} object. Tiers whose upper bound is zero or below the lower one are ignored.
Response fields data — 6
keystringThe metric key.
enabledboolWhether usage is billed.
includedfloatThe free allowance.
max_valueintThe ceiling applied.
schemestringThe pricing scheme.
pricingobjectThe tiers after normalising. What you sent as an array comes back as an object keyed by the bounds.
Errors 5
not_found404No such service.
metric_required422The metric key was empty.
invalid_metric422The module cannot measure that metric. Checked only on the first configuration.
currency_required422There is no active currency with a price to turn it on.
insufficient_scope403The key lacks the required scope.
Request
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,
]);
Response
{
  "error": {
    "code": "currency_required",
    "message": "Enabling a metric requires at least one currency with a price."
  }
}

Removing a Metric

delete/api/v1/admin/services/{id}/metrics/{metric}
Services/DeleteServiceMetric admin the history stays

Takes the metric out of the service configuration. The usage and billing records stay put.

Response fields data — 2
deletedboolWhether it was removed.
metricstringKey of the metric removed.
Errors 2
not_found404The metric is not configured on this service.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/services/{id}/metrics/{metric}/usage
Services/GetServiceMetricUsage admin 370 days at most

Returns the daily usage points for the period you give.

Query parameters 2
period_startdaterequiredThe first day of the period.
period_enddaterequiredThe last day of the period. It cannot precede the start, and the span cannot exceed 370 days.
Response fields data — 4
metricstringThe metric key.
period_startdateThe start of the period.
period_enddateThe end of the period.
pointsobject[]The daily points, each a date and a value. A day with no reading comes back empty, not zero.
Errors 4
not_found404No such service.
invalid_period422A date could not be read, or the end precedes the start.
period_too_long422The span exceeds 370 days.
insufficient_scope403The key lacks the required scope.
Request
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-30
const 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

get/api/v1/admin/services/{id}/metrics/{metric}/billing
Services/GetServiceMetricBilling admin

Returns the period records raised for the metric.

Response fields data[] — 13
idintId of the billing row.
metricstringThe metric key.
period_startdateThe start of the period.
period_enddateThe end of the period.
total_usagefloatTotal usage in the period.
includedfloatThe free allowance.
overagefloatThe part beyond the allowance, which is what gets billed.
unit_pricefloatThe unit price.
amountfloatThe amount.
currency_idintCurrency id.
invoice_idintId of the invoice it went on. Zero while it has not been attached to one.
statusstringThe status of the row.
created_atstringWhen the row was created.
Errors 2
not_found404No such service.
insufficient_scope403The key lacks the required scope.
Request
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

Tiers go in as an array and come back as an object

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.

An empty value does not mean zero usage

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.

Removing a metric does not delete its history

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 it on needs a currency with a price

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.