The Services You Own

3 views Markdown

The eleven endpoints that read, set, renew and end a service you own.

Overview

A service is the thing a customer owns: a hosting package, a server, a software licence. These eleven endpoints read that record, change its settings, renew it and ask for it to end.

Renewal runs in two steps: these endpoints raise an invoice and payment happens on the invoice endpoints. Automatic renewal, meanwhile, is governed by two switches, one at the account level and one on the service.

Some products are charged by use as well. A meter can be switched, and an unpaid usage invoice locks it shut.

Reference

Listing the Services

get/api/v1/client/services
Services/GetServices the key's owner

Returns the services the account owns.

Query 5
pageintWhich page.
limitintRows per page. 100 at the most.
statusstringThe state filter. It takes badge groups: pending covers both waiting and in process, and cancelled takes completed in too.
typestringThe product type filter.
searchstringSearches the service name and the domain tied to it.
Response fields data[] — 10 + meta — 4
idintThe service id.
typestringThe product type.
namestringThe service name. It is a copy of the product name at order time.
statusstringThe raw state.
hoststringThe domain or host name tied to it.
product_idintThe id of the product ordered.
auto_renewboolWhether automatic renewal is on for this service.
priceobjectThe amount per period.
cyclestringThe billing cycle.
created_atstringThe day it was set up.
due_datestringThe day the next payment falls.
totalintHow many services there are. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services?status=active' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/services?status=active', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const soon = data.filter((s) => s.due_date && s.due_date < horizon);
$ch = curl_init('https://panel.example.com/api/v1/client/services?status=active');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The filter takes a BADGE GROUP rather than a raw state: pending covers two raw states at once.
$rows = Kernel::internal('client:Services/GetServices',
    ['owner_id' => $uid, 'status' => 'pending'])['data'];

Reading a Service

get/api/v1/client/services/{id}
Services/GetService the key's owner

Returns a service's settings, access details and where renewal stands.

Response fields data — 16
idintThe service id.
typestringThe product type.
namestringThe service name.
statusstringThe raw state.
productobjectThe product plan. It carries an id and a name.
hoststringThe domain or host name tied to it.
order_idintThe order that brought it into being.
managedboolWhether a panel module runs it. True means the tools and sign-in endpoints work.
auto_renewboolAutomatic renewal for this service.
auto_renew_lockedboolWhether account-wide automatic payment is on. The per-service switch cannot move while it is.
billing_profile_idintThe billing profile assigned. Zero means the account's default address.
priceobjectThe amount per period.
cyclestringThe billing cycle.
accessobjectThe access details as they stood at order time. Its keys follow the service type and an empty value never appears.
renewalobjectWhere renewal stands.
availableboolWhether a renewal invoice can be raised.
blocked_by_subscriptionboolWhether a subscription collects the renewals itself.
already_invoicedboolWhether the period is already invoiced.
open_invoice_idintThe open invoice it would attach to.
cyclesarrayThe renewal cycles that can be picked.
cancellationobjectAn open cancellation request. It carries a state, an urgency, a reason, a note and when it was asked.
has_metricsboolWhether a meter charged by use is defined.
Errors 2
not_found404No such service, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services/622' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
if (data.renewal.available) showRenewButton(data.renewal.cycles);
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A DOMAIN is NOT read here: domains have endpoints of their own and answer not found on this one.
$s = Kernel::internal('client:Services/GetService', ['owner_id' => $uid, 'id' => $id])['data'];
$panel = $s['managed'];

Changing the Service Preferences

patch/api/v1/client/services/{id}
Services/UpdateService two fields

Changes automatic renewal and the billing profile.

Body 2
auto_renewboolAutomatic renewal for this service. It cannot move while account-wide automatic payment is on.
billing_profile_idintThe billing profile id. Zero returns to the account default.
Response fields data — 16
dataobjectThe service as it now stands. Same shape as the read endpoint.
Errors 6
not_found404No such service, or it belongs to another customer.
nothing_to_update422The body holds no field that is known.
not_actionable422Automatic renewal was sent while the service is not live.
autorenew_locked422Account-wide automatic payment is on.
no_auto_pay_source422Turning it on was tried with no source to charge.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/client/services/622' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"auto_renew":true,"billing_profile_id":3}'
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ auto_renew: true }),
});

if (res.status === 422) explainLock(await res.json());
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['auto_renew' => true]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The per-service switch is LOCKED while account-wide automatic payment is on; look at the wallet setting first.
$s = Kernel::internal('client:Services/GetService', ['owner_id' => $uid, 'id' => $id])['data'];
if (! $s['auto_renew_locked'])
    Kernel::internal('client:Services/UpdateService',
        ['owner_id' => $uid, 'id' => $id, 'auto_renew' => true]);

Raising a Renewal Invoice

post/api/v1/client/services/{id}/renew
Services/RenewService it raises an invoice

Raises a renewal invoice for the service.

Body 1
cyclestringThe renewal cycle. It is picked from the cycle list in the service detail, and the current one stands in when left out.
Response fields data — 8
invoice_idintThe renewal invoice id.
numberstringThe invoice number shown.
statusstringThe raw invoice state.
statestringThe state shown to the customer.
totalobjectThe invoice total.
created_atstringThe day it was raised.
due_datestringThe day it falls due.
existingboolWhether an invoice that was already open came back. True means no new document was raised.
Errors 7
not_found404No such service, or it belongs to another customer.
not_renewable422The service is neither live nor expired.
renew_blocked_subscription422A subscription collects the renewals itself.
cycle_invalid422The cycle is not one the product renews at.
already_invoiced422The period is invoiced and no open invoice waits.
renew_disabled422The operator closed renewal on this service.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/services/622/renew' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"cycle":"annually"}'
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/renew`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ cycle }),
});

const { data } = await res.json();
if (data.existing) note('An invoice was already open.');
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/renew');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['cycle' => $cycle]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// It RAISES an invoice and PAYS nothing: hand the id it returns to the invoice payment endpoint.
$inv = Kernel::internal('client:Services/RenewService',
    ['owner_id' => $uid, 'id' => $id])['data'];

Kernel::internal('client:Invoices/PayInvoice',
    ['owner_id' => $uid, 'id' => $inv['invoice_id'], 'payment' => ['method' => 'balance']]);

Asking to Cancel

post/api/v1/client/services/{id}/cancellation
Services/CreateCancellation one request at a time

Opens a request to cancel the service.

Body 4
urgencystringreqWhen it should end: at once or at the end of the period.
reasonstringreqThe reason. One of five: no longer needed, too costly, moving away, missing features or other.
reason_detailstringA free explanation. It is required where the reason is other.
notestringA note to the operator.
Response fields data
dataobjectThe cancellation opened. Same shape as the cancellation field in the service detail.
Errors 5
not_found404No such service, or it belongs to another customer.
not_actionable422The service is not live.
cancellation_exists422A request is already open.
reason_detail_required422The explanation is empty on the other reason, or a value is invalid.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/services/622/cancellation' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"urgency":"period-ending","reason":"not-needed"}'
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/cancellation`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ urgency: 'period-ending', reason, note }),
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/cancellation');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['urgency' => 'period-ending', 'reason' => $why]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Choosing AT ONCE ends the service there and then: the period-end choice lets the time already paid run out.
Kernel::internal('client:Services/CreateCancellation',
    ['owner_id' => $uid, 'id' => $id, 'urgency' => 'period-ending', 'reason' => 'not-needed']);

Withdrawing a Cancellation

delete/api/v1/client/services/{id}/cancellation
Services/RevokeCancellation the key's owner

Withdraws an open cancellation request.

Response fields data — 2
revokedboolWhether the withdrawal ran.
service_idintThe service id.
Errors 2
not_found404No request stands open on this service.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/client/services/622/cancellation' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/cancellation`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${clientKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/cancellation');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Once a request is APPROVED withdrawing may not save the service; read its state from the detail.
$s = Kernel::internal('client:Services/GetService', ['owner_id' => $uid, 'id' => $id])['data'];
$pending = ($s['cancellation']['status'] ?? '') === 'pending';

Reading the Service Add-ons

get/api/v1/client/services/{id}/addons
Services/GetServiceAddons the key's owner

Returns the add-ons tied to the service.

Response fields data[] — 11
idintThe add-on record id.
addon_idintThe add-on definition id.
option_idintThe choice picked.
namestringThe add-on name.
optionstringThe choice name.
quantityintHow many were taken.
statusstringWhere the add-on stands.
priceobjectThe line total per period.
cyclestringThe billing cycle.
due_datestringThe day the next payment falls.
unpaid_invoice_idintThe unpaid purchase invoice. It fills while the add-on waits.
cancellation_requestedboolWhether it is set to end with the period.
Errors 2
not_found404No such service, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services/622/addons' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/addons`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const waiting = data.filter((a) => a.unpaid_invoice_id > 0);
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/addons');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A waiting add-on stays asleep until its INVOICE is paid; take the id from here and pay it.
$rows = Kernel::internal('client:Services/GetServiceAddons',
    ['owner_id' => $uid, 'id' => $id])['data'];
$due = array_filter(array_column($rows, 'unpaid_invoice_id'));

Reading the Service Invoices

get/api/v1/client/services/{id}/invoices
Services/GetServiceInvoices the key's owner

Returns the documents that bill this service.

Query 1
limitintThe rows at most. 100 at the most.
Response fields data[] — 7
invoice_idintThe invoice id.
numberstringThe number shown.
statusstringThe raw state.
statestringThe state shown.
totalobjectThe document total.
created_atstringThe day it was raised.
due_datestringThe day it falls due.
Errors 2
not_found404No such service, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services/622/invoices' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/invoices`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const open = data.filter((i) => i.state !== 'paid');
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/invoices');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// There is NO PAGING here, only a ceiling: use the invoice listing for a long history.
$rows = Kernel::internal('client:Services/GetServiceInvoices',
    ['owner_id' => $uid, 'id' => $id, 'limit' => 100])['data'];

Reading the Meters

get/api/v1/client/services/{id}/metrics
Services/GetServiceMetrics charged by use

Returns the meters charged by use and what they are likely to cost.

Response fields data — 2
metricsobject[]The meters.
keystringThe meter key. This is what the switch endpoint takes.
labelstringThe name shown.
unitstringIts unit.
enabledboolWhether it is measuring now.
includedfloatWhat is included free.
usagefloatThe use this period.
overagefloatThe billable use above what is included.
estimated_chargeobjectThe charge expected for the overage.
unit_priceobjectThe unit price of the first live tier.
lockedboolWhether an unpaid usage invoice blocks turning it on.
locked_by_invoice_idintThe invoice blocking it.
auto_disabledboolWhether it closed by itself over an unpaid invoice.
has_unpaidboolWhether any usage invoice on the service is unpaid. It is a lock across the whole service.
Errors 2
not_found404No such service, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services/622/metrics' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/metrics`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const bill = data.metrics.reduce((s, m) => s + (m.estimated_charge?.amount ?? 0), 0);
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/metrics');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A meter switched off is still billed for what it USED UP TO THEN: closing it erases no history.
$m = Kernel::internal('client:Services/GetServiceMetrics',
    ['owner_id' => $uid, 'id' => $id])['data'];
$blocked = $m['has_unpaid'];

Switching a Meter

patch/api/v1/client/services/{id}/metrics/{key}
Services/UpdateServiceMetric an unpaid invoice locks it

Turns one meter on or off.

Body 1
enabledboolreqThe state wanted.
Response fields data
dataobjectThe meter as it now stands.
Errors 7
not_found404No such meter key or service.
enabled_invalid422The state field is missing or of the wrong kind.
not_actionable422The service is not live.
metric_not_priced422No live price tier exists.
metric_unpaid_lock422An unpaid usage invoice blocks turning it on.
metric_module_rejected422The panel module refused the change.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/client/services/622/metrics/storage' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true}'
const res = await fetch(`https://panel.example.com/api/v1/client/services/${id}/metrics/${key}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ enabled: true }),
});

if (res.status === 422) explainLock(await res.json());
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/metrics/' . $key);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['enabled' => true]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The one way past the lock is PAYING THE INVOICE that blocks it; its id is written on the meter.
$m = Kernel::internal('client:Services/GetServiceMetrics',
    ['owner_id' => $uid, 'id' => $id])['data'];
$blocker = array_column($m['metrics'], 'locked_by_invoice_id', 'key')[$key] ?? 0;

The Usage Billing History

get/api/v1/client/services/{id}/metrics/billing
Services/GetServiceMetricBilling the key's owner

Returns the use billed period by period.

Query 3
pageintWhich page.
limitintRows per page. 100 at the most.
metricstringNarrows it to one meter.
Response fields data[] — 9 + meta — 4
metricstringThe meter key.
labelstringThe name shown.
unitstringIts unit.
period_startstringThe start of the period billed.
period_endstringThe end of it.
usagefloatThe use over the period.
overagefloatThe share billed.
amountobjectThe amount.
statestringWhere the row stands: pending, unpaid, paid or cancelled.
invoice_idintThe usage invoice. It is zero while nothing was billed.
totalintHow many rows there are. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
Errors 2
not_found404No such service, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/services/622/metrics/billing?metric=storage' \
  -H "Authorization: Bearer $CLIENT_KEY"
const url = new URL(`https://panel.example.com/api/v1/client/services/${id}/metrics/billing`);
url.searchParams.set('metric', key);

const res = await fetch(url, { headers: { Authorization: `Bearer ${clientKey}` } });
const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/services/' . $id . '/metrics/billing');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A PENDING row is a period not yet billed: its invoice id is zero and the amount can still move.
$rows = Kernel::internal('client:Services/GetServiceMetricBilling',
    ['owner_id' => $uid, 'id' => $id])['data'];
$open = array_filter($rows, fn ($r) => $r['state'] === 'pending');

Pitfalls

Renewal raises an invoice and pays nothing

The renewal endpoint produces an invoice and stops there; the service runs longer only once that invoice is paid. A field in the answer says whether no new document was raised and an already open one came back. Hand the id to the invoice payment endpoint.

Two automatic renewal switches exist

While account-wide automatic payment is on the per-service switch is locked and a change is refused. The lock field in the detail says so beforehand. The way past it is the wallet setting rather than the service one.

Cancelling at once burns the time paid for

The urgency on a cancellation takes one of two values. At once ends the service there and then and the time already paid for does not come back, while at the end of the period lets it run out. Show the customer that difference.

Switching a meter off erases no past use

Switching a meter off does not save the use up to that moment from being billed: it still enters the document at period end. An unpaid usage invoice also locks the meter shut, and paying that invoice is the only key.

Domains do not belong to these endpoints

A domain record looks like a service and answers not found on these endpoints, because it has endpoints of its own. Code looking for a domain in a service listing comes back empty and can read that as a fault. See the domain articles instead.

The service invoice list is not paged

The endpoint giving a service's invoices takes a ceiling alone and no paging. Use the general invoice listing for a long history and search there. The add-on listing carries the same limit.

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.