Add-ons on a Service
The eight endpoints that attach, edit and change the state of add-ons sold on top of a service.
Overview
An add-on is something sold on top of a service: extra disk, backups, WHOIS privacy. It has its own price, its own cycle and its own status; it lives with the service but can be suspended and cancelled apart from it.
There are two sources. Catalogue add-ons are defined on the product side and attached by id. Domain add-ons never enter the catalogue; they are DNS management, e-mail forwarding and WHOIS privacy, and they are attached by key.
Reference
Listing the Add-ons
Returns every add-on hanging on the service.
waiting, inprocess, active, suspended or cancelled.curl 'https://panel.example.com/api/v1/admin/services/529/addons' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Services()->GetServiceAddons(['id' => 529]);Add-on Detail
Returns one add-on. The schema is the same as a list item.
waiting, inprocess, active, suspended or cancelled.curl 'https://panel.example.com/api/v1/admin/services/529/addons/44' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The id in the path is the RECORD'S, not the catalogue definition's.
$response = Api::Services()->GetServiceAddon(['id' => 529, 'addon_id' => 44]);Adding an Add-on
Attaches an add-on to the service, either from the catalogue or specific to a domain.
dns-manage, email-forwarding or whois-privacy. On domain services only, and instead of addon_id.waiting, inprocess or active. Defaults to waiting.unpaid or paid. Defaults to unpaid.curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"addon_id":9,"option_id":3,"quantity":1,"cycle":"monthly","generate_invoice":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
addon_id: 9,
option_id: 3,
quantity: 1,
cycle: 'monthly',
generate_invoice: true,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'addon_id' => 9,
'option_id' => 3,
'quantity' => 1,
'cycle' => 'monthly',
'generate_invoice' => true,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// On a domain service you send addon_key, NOT addon_id.
$service = Api::Services()->GetService(['id' => 529])['data'];
$body = $service['type'] === 'domain'
? ['addon_key' => 'whois-privacy']
: ['addon_id' => 9, 'option_id' => 3];
Api::Services()->CreateServiceAddon(['id' => 529] + $body);Updating an Add-on
Changes an add-on's price, quantity, cycle and dates.
curl -X PATCH 'https://panel.example.com/api/v1/admin/services/529/addons/44' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"amount":15,"quantity":2}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 15, quantity: 2 }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['amount' => 15, 'quantity' => 2]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Moving the service's term shifts add-ons on the same day; this endpoint moves ONE add-on.
$response = Api::Services()->UpdateServiceAddon([
'id' => 529,
'addon_id' => 44,
'end_date' => '2026-09-01 00:00:00',
]);Deleting an Add-on
Deletes the add-on record.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/529/addons/44' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44');
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);// Deleting switches nothing OFF in the module; for the provider side call cancel first.
Api::Services()->CancelServiceAddon([
'id' => 529,
'addon_id' => 44,
'apply_on_module' => true,
]);
Api::Services()->DeleteServiceAddon(['id' => 529, 'addon_id' => 44]);Suspending an Add-on
Stops the add-on. It does not touch the service itself.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/suspend' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"reason":"Awaiting payment","notify":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44/suspend', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ reason: 'Awaiting payment', notify: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44/suspend');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['reason' => 'Awaiting payment']),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Services()->SuspendServiceAddon([
'id' => 529,
'addon_id' => 44,
'reason' => 'Awaiting payment',
]);
// If the status changed but it never reached the module, the feature KEEPS working.
$reached = $response['data']['applied_on_module'] ?? false;Unsuspending an Add-on
Puts a suspended add-on back to work.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/unsuspend' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44/unsuspend', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44/unsuspend');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Services()->UnsuspendServiceAddon(['id' => 529, 'addon_id' => 44]);Cancelling an Add-on
Cancels the add-on and, if you ask, switches it off at the provider.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/cancel' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"apply_on_module":true,"notify":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ apply_on_module: true, notify: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44/cancel');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
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);// On cancel the flag is OFF BY DEFAULT: leave it out and the feature stays on at the server.
$response = Api::Services()->CancelServiceAddon([
'id' => 529,
'addon_id' => 44,
'apply_on_module' => true,
]);Pitfalls
A catalogue add-on is attached with addon_id, a domain one with addon_key, and neither stands in for the other. Sending an id on a domain service answers addon_not_found; sending a key on any other answers addon_id_required. Read the service type before attaching.
On suspend and unsuspend, applying at the provider follows whether the service has a module; on cancel it is off. Cancel without the flag and the record closes while the feature stays switched on at the server. That asymmetry is easy to miss.
The delete removes the record and never touches the provider; it does not even take a module flag. If the add-on has to be switched off at the server too, call cancel with the flag first and delete afterwards.
The id on the sub-endpoints is the record attached to the service, not the catalogue definition. Both come back side by side in the list: id is the record and addon_id is the definition. You attach with the definition's id and use the record's id for everything after.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.