# Product Requirements

https://dev.wisecp.com/es/product-requirements

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.

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

```bash
curl -G 'https://panel.example.com/api/v1/admin/products/requirements' \
  -H "Authorization: Bearer $API_KEY" \
  -d group=server
```

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

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

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

statusstring`active` 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.

langsobject 3 fieldsA map from language code to a content object.

namestringThe requirement name.

descriptionstringThe description the client sees.

optionsobject[] 3 fieldsThe 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/products/requirements/42' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

```php
$response = Api::Products()->GetProductRequirement(['id' => 42]);

// An empty mapping means the answer NEVER reaches the module.
$mapped = ($response['data']['module_co_names']['HetznerCloud'] ?? '') !== '';
```

Response 200

```json
{
  "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`.

statusstring`active` 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_required422`group` was empty.

category_required422`category` was empty.

name_required422There is no name in the current language.

create_failed422Creation was refused.

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

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

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

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

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

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

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

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

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

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/requirements/7' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

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

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

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

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

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

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/requirements/7/icon' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

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

```bash
curl 'https://panel.example.com/api/v1/admin/products/requirement-categories' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

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

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

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

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

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

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

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

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

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

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/requirement-categories/4' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

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

## Related Articles

- [Product Endpoints](https://dev.wisecp.com/en/product-endpoints)
- [Add-on Definitions](https://dev.wisecp.com/en/addon-definitions)
