Product Requirements

8 Aufrufe Markdown

The eleven endpoints that manage the fields a product asks at order time, their rules, icons and categories.

Overview

A requirement is a field the product asks the client to fill at order time: a hostname, a domain, a licence key, an install script. One thing separates it from an add-on: it has no price. An add-on is an option being sold, a requirement is a question being asked.

What matters is where the answer goes. The module_co_names mapping says which field in which module the client's answer lands in. Without a mapping the answer is stored but never used when the service is provisioned.

Reference

Listing the Requirements

get/api/v1/admin/products/requirements
Products/GetProductRequirements admin paged

Returns the requirement definitions in the catalogue.

Query parameters 5
groupstringFilters by the main group key.
categoryintFilters by requirement category.
searchstringSearches the requirement name.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 7
idintRequirement id.
namestringThe requirement name in the current language.
descriptionstringThe description.
groupstringThe main group key.
categoryintId of the requirement category.
statusstringactive or inactive.
rankintThe display order.
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/requirements' \
  -H "Authorization: Bearer $API_KEY" \
  -d group=server
const url = new URL('https://panel.example.com/api/v1/admin/products/requirements');
url.searchParams.set('group', 'server');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/products/requirements?' . http_build_query(['group' => 'server']);

$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()->GetProductRequirements([], ['group' => 'server']);

Requirement Detail

get/api/v1/admin/products/requirements/{id}
Products/GetProductRequirement admin module mapping

Returns one requirement with its rules, module mapping and options in every language.

Response fields data — 9
idintRequirement id.
groupstringThe main group key.
categoryintId of the requirement category.
statusstringactive or inactive.
rankintThe display order.
module_co_namesobjectA map from module name to the field name in that module. An empty value means unmapped: the answer is stored but never reaches the module.
typestringThe field type the client sees: text, textarea, select, radio, checkbox or file.
propertiesobjectThe rules for the type: whether it is compulsory, the maximum file size, the allowed extensions.
langsobjectA map from language code to a content object.
namestringThe requirement name.
descriptionstringThe description the client sees.
optionsobject[]The options in this language. Filled only on the choice types; empty on the rest.
idintOption id.
namestringThe option's name in that language.
mkeystringWhat the option maps to in the module. It can be left empty.
Errors 2
not_found404No such requirement.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/requirements/42' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/requirements/42', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/requirements/42');
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()->GetProductRequirement(['id' => 42]);

// An empty mapping means the answer NEVER reaches the module.
$mapped = ($response['data']['module_co_names']['HetznerCloud'] ?? '') !== '';
Response
{
  "data": {
    "id": 42,
    "group": "server",
    "category": 297,
    "status": "active",
    "rank": 0,
    "module_co_names": { "HetznerCloud": "user_data" },
    "type": "textarea",
    "properties": { "compulsory": false },
    "langs": {
      "en": {
        "name": "User Data",
        "description": "Cloud-init data run at first boot.",
        "options": []
      }
    }
  }
}

Creating a Requirement

post/api/v1/admin/products/requirements
Products/CreateProductRequirement admin 201

Opens a new requirement definition in the catalogue.

Body 12
groupstringrequiredThe main group key. It comes from the groups lookup.
categoryintrequiredId of the requirement category.
nameobjectrequiredA map from language code to name. The current language needs one.
descriptionobjectA map from language code to description.
typestringField type: text, textarea, select, radio, checkbox or file. Defaults to text.
statusstringactive or inactive.
rankintThe display order.
compulsoryboolMakes the field compulsory. Left empty, the order cannot go through.
max_file_sizeintThe maximum file size. Meaningful only on the file type.
allowed_extensionsstringThe allowed file extensions. Meaningful only on the file type.
module_co_namesobjectA map from module name to the field name in that module.
optionsobjectA map from option key to a {name, mkey} object. Choice types only; sending it replaces the whole set.
Response fields data — 9
dataobjectThe requirement created. 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/requirements' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"group":"server","category":4,"type":"text","name":{"en":"Server Hostname"},"compulsory":true}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/requirements', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    group: 'server',
    category: 4,
    type: 'text',
    name: { en: 'Server Hostname' },
    compulsory: true,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/requirements');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'group'      => 'server',
        'category'   => 4,
        'type'       => 'text',
        'name'       => ['en' => 'Server Hostname'],
        'compulsory' => true,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Give the mapping too, or the answer lands nowhere in the module.
$response = Api::Products()->CreateProductRequirement([
    'group'           => 'server',
    'category'        => 4,
    'type'            => 'text',
    'name'            => ['en' => 'Server Hostname'],
    'module_co_names' => ['HetznerCloud' => 'hostname'],
]);

Updating a Requirement

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

Applies the fields you send and leaves the rest as they were. If you send the options, the set is replaced as a whole.

Body 12
groupstringThe main group key. It comes from the groups lookup.
categoryintId of the requirement category.
nameobjectA map from language code to name. Merged language by language.
descriptionobjectA map from language code to description.
typestringField type: text, textarea, select, radio, checkbox or file. Leave it out and the stored type stays.
statusstringactive or inactive.
rankintThe display order.
compulsoryboolMakes the field compulsory. Left empty, the order cannot go through.
max_file_sizeintThe maximum file size. Meaningful only on the file type.
allowed_extensionsstringThe allowed file extensions. Meaningful only on the file type.
module_co_namesobjectA map from module name to the field name in that module.
optionsobjectA map from option key to a {name, mkey} object. Choice types only; sending it replaces the whole set.
Response fields data — 9
dataobjectThe requirement as it now stands. Same shape as the detail endpoint.
Errors 3
not_found404No such requirement.
update_failed422The update was refused.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/products/requirements/7' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"inactive","compulsory":false}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/requirements/7', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: 'inactive', compulsory: false }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/requirements/7');
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',
        'compulsory' => false,
    ]),
]);

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

// ... change $options ...

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

Deleting a Requirement

delete/api/v1/admin/products/requirements/{id}
Products/DeleteProductRequirement admin cannot be undone

Deletes the requirement definition. Its language records go too.

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

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

Uploading a Requirement Icon

post/api/v1/admin/products/requirements/{id}/icon
Products/UploadRequirementIcon admin SVG

Uploads the requirement 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 requirement.
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/requirements/7/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/requirements/7/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/requirements/7/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()->UploadRequirementIcon([
    'id'    => 7,
    'image' => 'data:image/svg+xml;base64,' . base64_encode($svg),
]);

Deleting a Requirement Icon

delete/api/v1/admin/products/requirements/{id}/icon
Products/DeleteProductRequirementIcon admin

Removes the uploaded icon image.

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

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

Listing the Categories

get/api/v1/admin/products/requirement-categories
Products/GetRequirementCategories admin

Returns the requirement 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/requirement-categories' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/requirement-categories', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/requirement-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()->GetRequirementCategories();

Creating a Category

post/api/v1/admin/products/requirement-categories
Products/CreateRequirementCategory admin 201

Opens a requirement 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. Only these two come back; read the row from the category listing 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/requirement-categories' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Server Info","rank":1}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/requirement-categories', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Server Info', rank: 1 }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/requirement-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' => 'Server Info', 'rank' => 1]),
]);

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

Api::Products()->CreateProductRequirement([
    'group'    => 'server',
    'category' => $category['data']['id'],
    'name'     => ['en' => 'Server Hostname'],
]);

Updating a Category

patch/api/v1/admin/products/requirement-categories/{id}
Products/UpdateRequirementCategory 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.
parent_idintId of the parent category.
rankintThe display order.
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/requirement-categories/4' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Server Details"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/requirement-categories/4', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Server Details' }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->UpdateRequirementCategory([
    'id'    => 4,
    'title' => 'Server Details',
]);

Deleting a Category

delete/api/v1/admin/products/requirement-categories/{id}
Products/DeleteRequirementCategory admin

Deletes the requirement 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/requirement-categories/4' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/requirement-categories/4', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

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

Pitfalls

An unmapped requirement never reaches the module

Asking the client is not enough. With no field name for that module in module_co_names, the answer sits in the record and is never used at provisioning. No error is raised either: the server comes up with the wrong name and nothing says why. Give the mapping when you open the requirement.

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. Leaving the field out keeps them.

Some fields only work on their own type

The maximum file size and the allowed extensions mean something only on the file type, and options only on the choice types. A rule sent to the wrong type raises no error; it is stored quietly and never applied.

A compulsory field stops the order

An order cannot go through while a requirement marked compulsory is left empty. Adding a compulsory requirement to a product that is already selling stops purchases of it there and then, so make sure the question can actually be answered on the storefront first.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.