Add-on Definitions

8 vues Markdown

The eleven endpoints that manage add-on definitions, their options, icons and categories.

Overview

An add-on definition is the template for something sold alongside a product: extra disk, automated backups, an extra licence. An add-on already attached to a client's service is a different thing; here you manage the definition in the catalogue.

A definition holds options, and that is where the price lives. Options sit under a language because their names are translated, while the price is kept per currency.

The last four endpoints run the add-on categories. A category does nothing on its own; it organises the add-on catalogue.

Reference

Listing the Add-on Definitions

get/api/v1/admin/products/addons
Products/GetProductAddons admin paged

Returns the add-on definitions in the catalogue.

Query parameters 5
groupstringFilters by the main group key.
categoryintFilters by add-on category.
searchstringSearches the add-on name.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 9
idintAdd-on id.
namestringThe add-on name in the current language.
descriptionstringThe description.
groupstringThe main group key.
categoryintId of the add-on category.
statusstringactive or inactive.
rankintThe display order.
icon_typestringfont or image.
iconstringThe icon class.
Meta 4
totalintTotal records matching the filter.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/products/addons' \
  -H "Authorization: Bearer $API_KEY" \
  -d group=hosting
const url = new URL('https://panel.example.com/api/v1/admin/products/addons');
url.searchParams.set('group', 'hosting');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/products/addons?' . http_build_query(['group' => '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);
$response = Api::Products()->GetProductAddons([], ['group' => 'hosting']);

Add-on Detail

get/api/v1/admin/products/addons/{id}
Products/GetProductAddon admin options sit under language

Returns one add-on definition with its settings, rules and options in every language.

Response fields data — 15
idintAdd-on id.
groupstringThe main group key.
categoryintId of the add-on category.
statusstringactive or inactive.
rankintThe display order.
override_user_currencyboolWhether it overrides the client's currency.
tax_exemptboolWhether it is tax exempt.
requirement_idsint[]Ids of the attached requirements.
product_linkobjectThe linked product as {id, type}.
icon_typestringfont or image.
iconstringThe icon class or the name of the uploaded image.
list_templateintId of the list template.
typestringThe input the client sees: select, quantity, checkbox or radio.
propertiesobjectThe rules for the input type: whether it is compulsory, multiple purchases, minimum and maximum quantity, the step, and tying the price to the product period.
langsobjectA map from language code to a content object.
namestringThe add-on name.
descriptionstringThe description.
optionsobject[]The options in this language.
idintOption id.
namestringThe option's name in that language.
periodstringday, month or year.
period_timeintThe period multiplier. Three months is period month, multiplier three.
amountfloatThe price. Present in the single-currency shape.
cidintCurrency id. Present in the single-currency shape.
pricingobjectA map from currency id to an {enabled, amount} object. Present in the multi-currency shape.
moduleobjectA map from module name to configuration. What gets handed to the module when the option is picked.
hiddenboolWhether the option is hidden.
Errors 2
not_found404No such add-on.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/addons/148' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/addons/148', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons/148');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetProductAddon(['id' => 148]);

// A price arrives in one of two shapes; handle both.
$option = $response['data']['langs']['en']['options'][0];
$price  = $option['pricing']['1']['amount'] ?? $option['amount'] ?? 0;
Response
{
  "data": {
    "id": 148,
    "group": "server",
    "category": 296,
    "status": "active",
    "type": "select",
    "properties": { "show_by_pp": 1, "multiple_purchases": 0 },
    "requirement_ids": [],
    "product_link": { "id": 0, "type": "" },
    "langs": {
      "en": {
        "name": "Automated Backups",
        "description": "Keeps daily backups.",
        "options": [
          {
            "id": 0,
            "name": "I want",
            "period": "month",
            "period_time": 1,
            "amount": 2,
            "cid": 5,
            "module": {
              "HetznerCloud": { "configurable": { "backup": 1 } }
            }
          }
        ]
      }
    }
  }
}

Creating an Add-on Definition

post/api/v1/admin/products/addons
Products/CreateProductAddon admin 201

Opens a new add-on definition in the catalogue.

Body 21
groupstringrequiredThe main group key. It comes from the groups lookup.
categoryintrequiredId of the add-on category.
nameobjectrequiredA map from language code to name. The current language needs one.
descriptionobjectA map from language code to description.
statusstringactive or inactive.
rankintThe display order.
typestringThe input shape: select, checkbox, radio or quantity.
compulsoryboolMakes the add-on compulsory.
min_quantityintThe minimum quantity. Meaningful on the quantity input.
max_quantityintThe maximum quantity.
stepintThe quantity step.
multiple_purchasesboolLets the same add-on be bought more than once.
show_by_product_periodboolShows the price against the product's period.
override_user_currencyboolOverrides the client's currency.
tax_exemptboolMakes it tax exempt.
requirement_idsint[]Ids of the requirements to attach.
product_linkintId of the product to link.
icon_typestringfont or image.
iconstringThe icon class.
list_templateintId of the list template.
optionsobjectA map from option key to an option object. Each option carries a name per language, a price per currency, a period, module configuration and whether it is hidden. Sending it replaces the whole set.
Response fields data
dataobjectThe add-on created, returned with 201. Same shape as the detail endpoint.
Errors 5
group_required422group was empty.
category_required422category was empty.
name_required422There is no name in the current language.
create_failed422Creation was refused.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/addons' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"group":"hosting","category":12,"type":"select","name":{"en":"Extra Disk"},"options":{"o1":{"name":{"en":"10 GB"},"pricing":{"1":{"enabled":true,"amount":5}},"cycle":"monthly"}}}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/addons', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    group: 'hosting',
    category: 12,
    type: 'select',
    name: { en: 'Extra Disk' },
    options: {
      o1: {
        name: { en: '10 GB' },
        pricing: { 1: { enabled: true, amount: 5 } },
        cycle: 'monthly',
      },
    },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'group'    => 'hosting',
        'category' => 12,
        'type'     => 'select',
        'name'     => ['en' => 'Extra Disk'],
        'options'  => [
            'o1' => [
                'name'    => ['en' => '10 GB'],
                'pricing' => [1 => ['enabled' => true, 'amount' => 5]],
                'cycle'   => 'monthly',
            ],
        ],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->CreateProductAddon([
    'group'    => 'hosting',
    'category' => 12,
    'type'     => 'select',
    'name'     => ['en' => 'Extra Disk'],
    'options'  => [
        'o1' => [
            'name'    => ['en' => '10 GB'],
            'pricing' => [1 => ['enabled' => true, 'amount' => 5]],
            'cycle'   => 'monthly',
        ],
    ],
]);

Updating an Add-on Definition

patch/api/v1/admin/products/addons/{id}
Products/UpdateProductAddon admin options are all-or-nothing

Applies the fields you send. If you send the options, the set is replaced as a whole. Every body field is optional.

Body 21
groupstringThe main group key. It comes from the groups lookup.
categoryintId of the add-on category.
nameobjectA map from language code to name. Only the languages you send change.
descriptionobjectA map from language code to description.
statusstringactive or inactive.
rankintThe display order.
typestringThe input shape: select, checkbox, radio or quantity.
compulsoryboolMakes the add-on compulsory.
min_quantityintThe minimum quantity. Meaningful on the quantity input.
max_quantityintThe maximum quantity.
stepintThe quantity step.
multiple_purchasesboolLets the same add-on be bought more than once.
show_by_product_periodboolShows the price against the product's period.
override_user_currencyboolOverrides the client's currency.
tax_exemptboolMakes it tax exempt.
requirement_idsint[]Ids of the requirements to attach.
product_linkintId of the product to link.
icon_typestringfont or image.
iconstringThe icon class.
list_templateintId of the list template.
optionsobjectA map from option key to an option object. Each option carries a name per language, a price per currency, a period, module configuration and whether it is hidden. Sending it replaces the whole set.
Response fields data
dataobjectThe add-on as it now stands. Same shape as the detail endpoint.
Errors 2
not_found404No such add-on.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/products/addons/131' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"inactive"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/addons/131', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: 'inactive' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons/131');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['status' => 'inactive']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Even to change one option's price, send the WHOLE set.
$addon   = Api::Products()->GetProductAddon(['id' => 131])['data'];
$options = $addon['langs']['en']['options'];

// ... change $options ...

Api::Products()->UpdateProductAddon([
    'id'      => 131,
    'options' => $options,
]);

Deleting an Add-on Definition

delete/api/v1/admin/products/addons/{id}
Products/DeleteProductAddon admin cannot be undone

Deletes the add-on definition. Its language records go too.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted add-on.
Errors 3
not_found404No such add-on.
blocked_by_gate422The gate:product.addon_delete hook vetoed the operation.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/addons/131' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/addons/131', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons/131');
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);
$response = Api::Products()->DeleteProductAddon(['id' => 131]);

Uploading an Add-on Icon

post/api/v1/admin/products/addons/{id}/icon
Products/UploadAddonIcon admin SVG

Uploads the add-on icon. Image files and SVG are accepted.

Body 1
imagestringrequiredThe icon image. A base64 data URI or a link that can be fetched.
Response fields data — 1
urlstringThe icon's public address.
Errors 5
not_found404No such add-on.
file_required422The file field was empty.
file_invalid422The file could not be read or its type was refused.
file_failed422The file could not be stored.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/addons/131/icon' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"image":"data:image/png;base64,iVBORw0KGgo..."}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/addons/131/icon', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ image: 'data:image/png;base64,iVBORw0KGgo...' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons/131/icon');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'image' => 'data:image/svg+xml;base64,' . base64_encode($svg),
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->UploadAddonIcon([
    'id'    => 131,
    'image' => 'data:image/svg+xml;base64,' . base64_encode($svg),
]);

Deleting an Add-on Icon

delete/api/v1/admin/products/addons/{id}/icon
Products/DeleteProductAddonIcon admin

Removes the uploaded icon image.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintAdd-on id.
Errors 2
not_found404No such add-on.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/addons/131/icon' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/addons/131/icon', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addons/131/icon');
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);
$response = Api::Products()->DeleteProductAddonIcon(['id' => 131]);

Listing the Categories

get/api/v1/admin/products/addon-categories
Products/GetAddonCategories admin

Returns the add-on categories.

Response fields data[] — 4
idintCategory id.
titlestringThe category title.
parent_idintId of the parent category. Zero means top level.
rankintThe display order.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/addon-categories' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/addon-categories', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addon-categories');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetAddonCategories();

Creating a Category

post/api/v1/admin/products/addon-categories
Products/CreateAddonCategory admin 201

Opens an add-on category.

Body 3
titlestringrequiredThe category title.
parent_idintId of the parent category.
rankintThe display order.
Response fields data — 2
idintId of the new category.
titlestringThe title, after HTML stripping. Two fields come back, so read the list endpoint for the parent and the order.
Errors 4
title_required422title was empty.
blocked_by_gate422The gate:product.category_save hook vetoed the operation.
create_failed422The insert failed.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/addon-categories' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Resources","rank":1}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/addon-categories', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Resources', rank: 1 }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addon-categories');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['title' => 'Resources', 'rank' => 1]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$category = Api::Products()->CreateAddonCategory(['title' => 'Resources']);

Api::Products()->CreateProductAddon([
    'group'    => 'hosting',
    'category' => $category['data']['id'],
    'name'     => ['en' => 'Extra Disk'],
]);

Updating a Category

patch/api/v1/admin/products/addon-categories/{id}
Products/UpdateAddonCategory admin

Changes the category's title, parent or order.

Body 3
titlestringThe category title. If you send it, it cannot be empty.
parent_idintId of the parent category.
rankintThe display order.
Response fields data — 4
idintCategory id.
titlestringThe category title.
parentintId of the parent category. Zero means top level. The list endpoint names this one parent_id.
rankintThe display order. The stored row comes back whole, so a few internal columns ride along.
Errors 4
not_found404No such category.
title_required422A title was sent but it was empty.
no_changes422The body carries no editable field.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/products/addon-categories/12' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"title":"System Resources","rank":2}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/addon-categories/12', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'System Resources', rank: 2 }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addon-categories/12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['title' => 'System Resources', 'rank' => 2]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// An empty body comes back as 'no_changes' - skip the call when nothing changed.
$response = Api::Products()->UpdateAddonCategory([
    'id'    => 12,
    'title' => 'System Resources',
]);

Deleting a Category

delete/api/v1/admin/products/addon-categories/{id}
Products/DeleteAddonCategory admin

Deletes the add-on category.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted category.
Errors 2
not_found404No such category.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/addon-categories/12' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/addon-categories/12', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/addon-categories/12');
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);
$response = Api::Products()->DeleteAddonCategory(['id' => 12]);

Pitfalls

Sending options rewrites the whole set

If options is in the update body, every existing option is dropped in every language and replaced by the set you sent. To change one option's price, read the detail first and send the set back whole. Leaving the field out keeps the options.

A price comes back in two different shapes

An option carries either a flat amount with a currency id, or a map of {enabled, amount} objects per currency. Which one you get depends on how the record was written, so reading code has to handle both.

Options are stored per language

Options live inside the language object, so each language holds its own copy. Adding an option to one language and not another leaves it invisible to clients using the other one.

An empty update body is an error

The category update refuses a body carrying no editable field and returns no_changes. When nothing changed, do not send the request at all.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.