Service Endpoints
The four endpoints that list, read, edit and delete client services.
Overview
A service is the thing a client bought and keeps: a hosting account, a domain, a server, a software licence. These four endpoints read, edit and delete the service itself.
Responses come back raw: statuses and cycles as codes, dates in a standard shape, amounts as numbers without a symbol. Labels for humans come from the reference endpoints.
The capabilities block on the detail says which operations this service accepts. The values are derived from the module, so they are not fixed.
Reference
Listing the Services
Returns the client services, with filters.
waiting, inprocess, active, suspended, expired, cancelled or completed.domain, hosting, server, software, sms, ssl or special. On special groups the id is appended to the type.curl -G 'https://panel.example.com/api/v1/admin/services' \
-H "Authorization: Bearer $API_KEY" \
-d status=active \
-d type=hostingconst url = new URL('https://panel.example.com/api/v1/admin/services');
url.searchParams.set('status', 'active');
url.searchParams.set('type', 'hosting');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();$url = 'https://panel.example.com/api/v1/admin/services?' . http_build_query(['status' => 'active', 'type' => 'hosting']);
$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);// The pagination fields sit at the ROOT, not under 'meta'.
$page = 1;
$all = [];
do {
$response = Api::Services()->GetServices([], [
'status' => 'active',
'page' => $page,
'limit' => 100,
]);
$all = array_merge($all, $response['data']);
$page = $response['next_page'];
} while ($page > 0);{
"data": [
{
"id": 510,
"name": "Mail Hosting",
"type": "hosting",
"type_id": 0,
"product_id": 15,
"domain": "example.com",
"status": "active",
"amount": 10.0,
"currency_id": 1,
"cycle": "monthly",
"qty": 1,
"module": "Mailcow",
"client": {
"id": 50,
"full_name": "John Doe",
"company_name": "",
"email": "[email protected]"
},
"created_at": "2026-06-18 12:00:00",
"due_at": "2026-07-18 12:00:00",
"renewal_at": "2026-07-18 12:00:00"
}
],
"total": 42,
"page": 1,
"limit": 25,
"next_page": 2
}Service Detail
Returns a service in full, with its relations and what can be done to it.
curl 'https://panel.example.com/api/v1/admin/services/506' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/506', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
// Read the capability before attempting the operation.
if (body.data.capabilities.can_reinstall) {
// ...
}$ch = curl_init('https://panel.example.com/api/v1/admin/services/506');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$service = Api::Services()->GetService(['id' => 506])['data'];
// Capabilities come from the module: most are off on a service without one.
if ($service['capabilities']['can_suspend'] ?? false) {
Api::Services()->SuspendService(['id' => 506]);
}Updating a Service
Changes a service's data fields. Status transitions do not happen here.
percent or amount.curl -X PATCH 'https://panel.example.com/api/v1/admin/services/506' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"Mail Hosting Pro","amount":29.9,"qty":2,"auto_pay":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/506', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Mail Hosting Pro',
amount: 29.9,
qty: 2,
auto_pay: true,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Mail Hosting Pro',
'amount' => 29.9,
'qty' => 2,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// An update that moves the term end also shifts add-ons falling on the same day.
$response = Api::Services()->UpdateService([
'id' => 506,
'due_at' => '2026-08-18 12:00:00',
]);Deleting a Service
Deletes the service record, and if you ask, closes the account at the provider too.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/510' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"apply_on_module":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/510', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ apply_on_module: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/510');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['apply_on_module' => true]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Without the flag the account on the server STAYS UP and keeps consuming resources.
$response = Api::Services()->DeleteService([
'id' => 510,
'apply_on_module' => true,
]);Pitfalls
By default the delete removes only the record; the account at the provider stays up and keeps consuming resources. Closing it too means putting the flag in the request. Once the record is gone there is no way left to do it through the API.
The update endpoint takes no status field: suspending, cancelling and reactivating live on their own endpoints. That is because those transitions make the module do work, and a plain field write would change nothing on the server.
The service list returns total, page, limit and next_page at the root. Many other lists put them under meta, so a shared pagination helper has to account for it.
Changing the term end also moves the end date of add-ons falling on the same day. That is usually what you want, but it is silent: you think you changed one date while other billable items moved with it.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.