Add-ons on a Service

8 views Markdown

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

get/api/v1/admin/services/{id}/addons
Services/GetServiceAddons admin

Returns every add-on hanging on the service.

Response fields data[] — 20
idintId of the add-on record. This is what the other endpoints use.
service_idintId of the service it hangs on.
addon_idintId of the catalogue add-on definition. Zero on a domain add-on.
addon_namestringThe add-on name.
option_idintId of the option chosen.
option_namestringThe option name.
statusstringwaiting, inprocess, active, suspended or cancelled.
amountfloatThe unit amount.
total_amountfloatThe amount multiplied by the quantity.
quantityintThe quantity.
currency_idintCurrency id.
cyclestringThe billing cycle.
periodstringThe period unit.
period_timeintThe period multiplier.
payment_methodstringThe payment method.
invoice_idintId of the invoice tied to it.
subscription_idintId of the subscription behind it.
created_atstringWhen it was added.
due_atstringWhen the term ends.
renewal_atstringThe renewal date.
Errors 2
not_found404No such service.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/services/{id}/addons/{addon_id}
Services/GetServiceAddon admin

Returns one add-on. The schema is the same as a list item.

Response fields data — 20
idintId of the add-on record. This is what the other endpoints use.
service_idintId of the service it hangs on.
addon_idintId of the catalogue add-on definition. Zero on a domain add-on.
addon_namestringThe add-on name.
option_idintId of the option chosen.
option_namestringThe option name.
statusstringwaiting, inprocess, active, suspended or cancelled.
amountfloatThe unit amount.
total_amountfloatThe amount multiplied by the quantity.
quantityintThe quantity.
currency_idintCurrency id.
cyclestringThe billing cycle.
periodstringThe period unit.
period_timeintThe period multiplier.
payment_methodstringThe payment method.
invoice_idintId of the invoice tied to it.
subscription_idintId of the subscription behind it.
created_atstringWhen it was added.
due_atstringWhen the term ends.
renewal_atstringThe renewal date.
Errors 2
not_found404No such service or add-on.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/addons
Services/CreateServiceAddon admin two sources

Attaches an add-on to the service, either from the catalogue or specific to a domain.

Body — which add-on 2
addon_idintId of the catalogue add-on definition. Needed on anything other than a domain.
addon_keystringA domain add-on: dns-manage, email-forwarding or whois-privacy. On domain services only, and instead of addon_id.
Body — price and term 9
option_idintId of the add-on option to pick.
option_namestringThe option's display name.
addon_namestringThe add-on's display name.
override_amountfloatSets the price by hand. Left out, the option's or the domain add-on's own price is used.
override_currencystringCurrency code of the hand-set price.
quantityintThe quantity. At least one, and it defaults to one.
cyclestringThe billing cycle. Fixed on a domain add-on and cannot be changed.
currency_idintCurrency id.
payment_methodstringThe payment method.
Body — dates, status and invoice 8
start_datedatetimeThe start date. Defaults to now.
end_datedatetimeThe end date.
renewal_datedatetimeThe renewal date.
statusstringThe starting status: waiting, inprocess or active. Defaults to waiting.
generate_invoiceboolProduces an invoice for the add-on.
invoice_statusstringStatus of the invoice produced: unpaid or paid. Defaults to unpaid.
subscription_identifierstringIdentifier of the subscription to tie it to.
requirementsobjectAnswers to the fields the add-on asks for. A map from requirement id to value.
Response fields 201 — data
dataobjectThe add-on created. Same shape as the detail endpoint.
Errors 6
not_found404No such service.
addon_id_required422No id was given for a catalogue add-on.
addon_invalid422The domain add-on key is not one of the three values.
addon_not_found422The add-on definition was not found.
addon_add_failed500The add-on could not be attached.
insufficient_scope403The key lacks the required scope.
Request
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

patch/api/v1/admin/services/{id}/addons/{addon_id}
Services/UpdateServiceAddon admin

Changes an add-on's price, quantity, cycle and dates.

Body 11
addon_namestringThe add-on's display name.
option_namestringThe option's display name.
amountfloatThe unit amount. The total is recalculated.
quantityintThe quantity. At least one.
currency_idintCurrency id.
cyclestringThe billing cycle. Not applied on a domain add-on.
payment_methodstringThe payment method.
subscription_identifierstringThe subscription identifier, matched to a subscription. An empty value or no match clears the link.
start_datedatetimeThe start date.
end_datedatetimeThe end date. Sending it empty clears the date.
renewal_datedatetimeThe renewal date. Sending it empty clears the date.
Response fields data
dataobjectThe add-on as it now stands. Same shape as the detail endpoint.
Errors 2
not_found404No such service or add-on.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/services/{id}/addons/{addon_id}
Services/DeleteServiceAddon admin cannot be undone

Deletes the add-on record.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted record.
Errors 2
not_found404No such service or add-on.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/addons/{addon_id}/suspend
Services/SuspendServiceAddon admin reaches the module

Stops the add-on. It does not touch the service itself.

Body 3
reasonstringWhy it was suspended.
notifyboolSends the client a notification.
apply_on_moduleboolApplies it at the provider too. Left out, whether the service has a module decides.
Response fields data — 3
statusstringThe status afterwards.
idintId of the add-on record.
applied_on_moduleboolWhether it reached the provider.
Errors 3
not_found404No such service or add-on.
status_change_failed500The status could not be changed.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/addons/{addon_id}/unsuspend
Services/UnsuspendServiceAddon admin reaches the module

Puts a suspended add-on back to work.

Body 2
notifyboolSends the client a notification.
apply_on_moduleboolApplies it at the provider too. Left out, whether the service has a module decides.
Response fields data — 3
statusstringThe status afterwards.
idintId of the add-on record.
applied_on_moduleboolWhether it reached the provider.
Errors 3
not_found404No such service or add-on.
status_change_failed500The status could not be changed.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/addons/{addon_id}/cancel
Services/CancelServiceAddon admin cannot be undone

Cancels the add-on and, if you ask, switches it off at the provider.

Body 3
reasonstringWhy it was cancelled.
notifyboolSends the client a notification.
apply_on_moduleboolSwitches it off at the provider too. Off by default here, unlike suspend, so you have to ask for it.
Response fields data — 3
statusstringThe status afterwards.
idintId of the add-on record.
applied_on_moduleboolWhether it reached the provider.
Errors 3
not_found404No such service or add-on.
status_change_failed500The status could not be changed.
insufficient_scope403The key lacks the required scope.
Request
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 domain add-on is attached by a different field

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.

The module flag is off by default on cancel

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.

Deleting switches nothing off at the server

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 in the path is the record's

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.

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.