# Add-ons on a Service

https://dev.wisecp.com/es/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

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.

statusstring`waiting`, `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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/services/529/addons' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/services/529/addons', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
$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.

statusstring`waiting`, `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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/services/529/addons/44' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/529/addons/44' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
$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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/unsuspend' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
$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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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.

## Related Articles

- [Add-on Definitions](https://dev.wisecp.com/en/addon-definitions)
- [Renewal and Cancellation](https://dev.wisecp.com/en/renewal-cancellation)
- [Service Endpoints](https://dev.wisecp.com/en/service-endpoints)
