# Product Endpoints

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

The nine endpoints that list the product catalogue and create, read, update and delete a product.

## Overview

These endpoints run the product itself in the catalogue: listing, creating, reading, updating and deleting. The three lookups beside them tell you which values are valid when creating.

There is a deliberate split between creating and updating: creation only puts up the **skeleton**, and everything that makes a product sellable goes in through the update endpoint. The panel follows the same order.

## Reference

### Listing the Products

get/api/v1/admin/products

`Products/GetProducts` admin paged

Returns the product catalogue. With no filter you get every type, newest first.

Query parameters 7

searchstringSearches the product title.

typestringFilters by type: `hosting`, `server`, `software`, `ssl` or `special`.

group_idintSpecial group id. Using it requires `type` to be `special`.

categoryintFilters by category id.

statusstring`active` or `inactive`.

pageintDefaults to 1.

limitintDefaults to 25, maximum 100.

Response fields data[] — 8

idintProduct id.

titlestringThe product title in the current language.

typestringThe product's behaviour type.

modulestringThe module attached. `none` when there is none.

statusstring`active` or `inactive`.

categoryobjectThe category as `{id, title}`. On an uncategorised product the title is empty.

service_countintHow many services were created from this product.

created_atstringWhen it was created.

Meta 4

totalintTotal records matching the filter.

pageintThe page you are on.

limitintThe page size.

next_pageintThe next page. `0` 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' \
  -H "Authorization: Bearer $API_KEY" \
  -d type=hosting \
  -d limit=50
```

```javascript
const url = new URL('https://panel.example.com/api/v1/admin/products');
url.searchParams.set('type', 'hosting');
url.searchParams.set('limit', '50');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
```

```php
$url = 'https://panel.example.com/api/v1/admin/products?' . http_build_query(['type' => 'hosting', 'limit' => 50]);

$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
$page = 1;
$all  = [];

do {
    $response = Api::Products()->GetProducts([], [
        'type'  => 'hosting',
        'page'  => $page,
        'limit' => 100,
    ]);

    $all  = array_merge($all, $response['data']);
    $page = $response['meta']['next_page'];
} while ($page > 0);
```

Response 200

```json
{
  "data": [
    {
      "id": 15,
      "title": "Starter SSD 1",
      "type": "hosting",
      "module": "cPanel",
      "status": "active",
      "category": { "id": 439, "title": "Economy Web Hosting" },
      "service_count": 12,
      "created_at": "2026-01-01 10:00:00"
    }
  ],
  "meta": { "total": 499, "page": 1, "limit": 25, "next_page": 2 }
}
```

### Creating a Product

post/api/v1/admin/products

`Products/CreateProduct` admin 201

Opens a skeleton product. Prices, limits and language content go in later with the update endpoint.

Body 6

typestringrequiredThe product type: `hosting`, `server`, `software` or `special`. `ssl` cannot stand alone; it lives inside a special group.

namestringrequiredThe product title. The same text is written to every language.

group_idintSpecial group id. Required when the type is `special`.

categoryintCategory id.

modulestringName of the module to attach. Giving one turns automatic provisioning on.

hiddenboolHides the product from the storefront.

Response fields data

dataobjectThe product created, returned with `201`. Same shape as the detail endpoint.

Errors 5

invalid_type422The type is not one of the allowed values.

name_required422`name` was empty.

group_required422The type is `special` but no group was given.

blocked_by_gate422The `gate:product.create` hook vetoed the operation.

create_failed422Creation was refused. The group may be missing or the route may collide.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/products' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type":"hosting","name":"Starter SSD 1","category":439,"module":"cPanel"}'
```

```javascript
const res = await fetch('https://panel.example.com/api/v1/admin/products', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'hosting',
    name: 'Starter SSD 1',
    category: 439,
    module: 'cPanel',
  }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'type'     => 'hosting',
        'name'     => 'Starter SSD 1',
        'category' => 439,
        'module'   => 'cPanel',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// Creation leaves a skeleton; follow it with an update for a usable product.
$created = Api::Products()->CreateProduct([
    'type'     => 'hosting',
    'name'     => 'Starter SSD 1',
    'category' => 439,
    'module'   => 'cPanel',
]);

Api::Products()->UpdateProduct([
    'id'     => $created['data']['id'],
    'limits' => ['disk' => 100, 'bandwidth' => 'unlimited'],
]);
```

### Product Detail

get/api/v1/admin/products/{id}

`Products/GetProduct` admin varies by module

Returns all of the product's settings, its relations and its content in every language.

Response fields data — 26

idintProduct id.

typestringThe product's behaviour type.

group_typestringThe group type key.

group_idintSpecial group id. 0 when it is not in one.

categoryobjectThe category as `{id, name}`.

statusstring`active` or `inactive`.

visibilitystring`visible` or `invisible`.

modulestringName of the attached module.

module_dataobjectThe module's own configuration. The fields depend entirely on the module; there is no fixed schema.

optionsobjectThe product options. The fields change with the type and the module.

additional_taxobjectAdditional tax as `{enabled, items}`.

tax_exemptboolWhether it is tax exempt.

override_user_currencyboolWhether it overrides the client's currency.

upgrade_enabledboolWhether upgrading is open.

affiliate_disabledboolWhether affiliate is switched off.

affiliate_ratefloatThe affiliate rate.

stockint | nullThe stock left. `null` means unlimited.

rankintThe display order.

upgradeable_product_idsint[]The products this one can be upgraded to.

addon_idsint[]Ids of the attached add-ons.

requirement_idsint[]Ids of the attached requirements.

notesstringAdmin notes. Never shown to the client.

prorateobjectDay-based proration as `{enabled, days}`.

recurring_cycles_limitobjectRenewal count limit as `{enabled, value}`.

auto_terminateobjectAutomatic termination as `{enabled, days}`.

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

titlestringThe product title.

descriptionstringA short description.

contentstringThe long content.

routestringThe URL slug.

featuresstringThe feature list.

seo_titlestringThe search title.

seo_keywordsstringThe search keywords.

seo_descriptionstringThe search description.

Errors 2

not_found404No such product.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

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

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/15');
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()->GetProduct(['id' => 15]);

// The shape of module_data comes from the module - check the key exists.
$plan = $response['data']['module_data']['plan'] ?? null;
```

### Updating a Product

patch/api/v1/admin/products/{id}

`Products/UpdateProduct` admin partial-safe

Applies the fields you send and leaves everything else as it was.

Body 22

statusstring`active` or `inactive`.

categoryintCategory id.

hiddenboolHides it from the storefront.

notesstringAdmin notes.

rankintThe display order.

stockint | nullThe stock. Leaving it empty means unlimited.

subdomainsstringThe products allowed for sub-hosting.

affiliate_disabledboolSwitches affiliate off.

affiliate_ratefloatThe affiliate rate.

override_user_currencyboolOverrides the client's currency.

tax_exemptboolMakes it tax exempt.

additional_taxobjectThe additional tax configuration.

allow_qtyboolLets the buyer pick a quantity.

prorateobject`{enabled, days}`.

recurring_cycles_limitobject`{enabled, value}`.

auto_terminateobject`{enabled, days}`.

addon_idsint[]Ids of the add-ons to attach.

requirement_idsint[]Ids of the requirements to attach.

upgradeable_product_idsint[]The upgrade target products.

optionsobjectProduct options: `popular`, `auto_approval`, `auto_install`, `seo_index`, `restrict_access`, `order_limit_per_user`, `free_domain`, `hide_domain`, `show_domain`, `hide_hosting`, `show_hosting`, `change_domain`, `renewal_selection_hide`, `download_link`, `demo_link`, `demo_admin_link`, `demo_admin_link`, `product_file_access`, `ctoc_service_transfer`, `server_group_id`, `server_id`, `activation_notification`.

limitsobjectHosting resource limits: `disk`, `bandwidth`, `email`, `database`, `addons`, `subdomain`, `ftp`, `park`, `max_email_per_hour`. Each is a number or `unlimited`.

langsobject 8 fieldsA map from language code to a content object. Only the languages you send change.

titlestringThe product title.

descriptionstringA short description.

contentstringThe long content.

routestringThe URL slug.

featuresstringThe feature list.

seo_titlestringThe search title.

seo_keywordsstringThe search keywords.

seo_descriptionstringThe search description.

Response fields data

dataobjectThe product as it now stands. Same shape as the detail endpoint.

Errors 2

not_found404No such product.

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/15' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"inactive","limits":{"disk":100,"bandwidth":"unlimited"}}'
```

```javascript
const res = await fetch('https://panel.example.com/api/v1/admin/products/15', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'inactive',
    limits: { disk: 100, bandwidth: 'unlimited' },
  }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/15');
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',
        'limits' => ['disk' => 100, 'bandwidth' => 'unlimited'],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// Changing the English text alone leaves the other languages untouched.
$response = Api::Products()->UpdateProduct([
    'id'    => 15,
    'langs' => [
        'en' => ['title' => 'Starter SSD 1', 'features' => 'NVMe storage'],
    ],
]);
```

### Changing Status in Bulk

post/api/v1/admin/products/bulk

`Products/BulkProducts` admin status only

Changes several products' status in one call. Deleting is not available here.

Body 2

idsint[]requiredThe product ids. At least one.

actionstringrequired`active` or `inactive`.

Response fields data — 2

updatedint[]The ids whose status changed.

actionstringThe status that was applied.

Errors 2

ids_required422`ids` was empty.

invalid_action422The action is neither of the two values.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/products/bulk' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"ids":[15,16],"action":"inactive"}'
```

```javascript
const res = await fetch('https://panel.example.com/api/v1/admin/products/bulk', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ids: [15, 16], action: 'inactive' }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'ids'    => [15, 16],
        'action' => 'inactive',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
$response = Api::Products()->BulkProducts([
    'ids'    => [15, 16],
    'action' => 'inactive',
]);
```

### Deleting a Product

delete/api/v1/admin/products/{id}

`Products/DeleteProduct` admin cannot be undone

Deletes the product. Its language records and prices go with it.

Response fields data — 2

deletedboolWhether the delete succeeded.

idintId of the deleted product.

Errors 2

not_found404No such product.

blocked_by_gate422The `gate:product.delete` hook vetoed the operation.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

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

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

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/2030');
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()->DeleteProduct(['id' => 2030]);
```

### Listing the Product Types

get/api/v1/admin/products/types

`Products/GetProductTypes` admin lookup

Returns the product types this installation offers. The valid values for creating come from here.

Response fields data[] — 4

keystringThe type key. This is what goes in the create body.

titlestringThe label to display.

descriptionstringWhat the type is.

iconstringThe icon class.

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/types' \
  -H "Authorization: Bearer $API_KEY"
```

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/types');
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()->GetProductTypes();
```

### Listing the Groups

get/api/v1/admin/products/groups

`Products/GetProductGroups` admin lookup

Returns the fixed and special catalogue groups. Each row hands you a ready type and group pair for creating.

Response fields data[] — 4

keystringThe group key. Special groups carry the id as a suffix.

titlestringThe label to display.

typestringThe product type this group maps to.

group_idintSpecial group id. 0 on a fixed group.

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/groups' \
  -H "Authorization: Bearer $API_KEY"
```

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/products/groups');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// A group row hands you both fields of the create body at once.
$group = Api::Products()->GetProductGroups()['data'][0];

Api::Products()->CreateProduct([
    'type'     => $group['type'],
    'group_id' => $group['group_id'],
    'name'     => 'New product',
]);
```

### Listing the Categories

get/api/v1/admin/products/categories

`Products/GetProductCategories` admin lookup

Returns the category list flat. You build the tree yourself from the parent ids.

Query parameters 2

typestringThe group type. Defaults to `hosting`.

group_idintSpecial group id. Anything above zero makes the type `special`.

Response fields data[] — 4

idintCategory id.

parent_idintId of the parent category. 0 means top level.

titlestringThe category title.

routestringThe URL slug.

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/categories' \
  -H "Authorization: Bearer $API_KEY" \
  -d type=hosting
```

```javascript
const url = new URL('https://panel.example.com/api/v1/admin/products/categories');
url.searchParams.set('type', 'hosting');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
```

```php
$url = 'https://panel.example.com/api/v1/admin/products/categories?' . http_build_query(['type' => '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);
```

```php
$rows = Api::Products()->GetProductCategories([], ['type' => 'hosting'])['data'];

$byParent = [];
foreach ($rows as $row) $byParent[$row['parent_id']][] = $row;
```

## Pitfalls

> **Creating does not give you a sellable product**
> 
> A new product is born as a skeleton: a zero monthly price is seeded in the default currency and the title is copied into every language. Until the limits, the real price and the content go in, the product is not ready to sell. Follow the create with an update.

> **The update leaves some sections out**
> 
> Cyclical pricing, metered pricing, module configuration and licence parameters cannot be **edited** here. They are rewritten with their current values, so leaving them out of your body does not wipe them. They are managed through their own endpoints.

> **Two fields have a module-dependent schema**
> 
> The `module_data` and `options` fields on the detail change with the attached module and the product type; they have no fixed schema. Code that reaches straight into them breaks when the module changes, so check the key exists.

> **The bulk endpoint does not delete**
> 
> The bulk action only changes status. Deleting products is one at a time, and every delete passes through the `gate:product.delete` hook, where an addon can veto it.

## Related Articles

- [Product Categories](https://dev.wisecp.com/en/product-categories)
- [Add-on Definitions](https://dev.wisecp.com/en/addon-definitions)
- [Product Media](https://dev.wisecp.com/en/product-media)
