Special Groups

7 views Markdown

The seven endpoints that manage the special groups holding product families outside the fixed types.

Overview

A special group holds together a family of products that falls outside the fixed types. SSL certificates are the canonical case: products with no type of their own live inside a special group.

A group is not only a container but a storefront setting. Payment gateways, the list template and the upgrade permission are set at group level and reach every product inside.

Reference

Listing the Groups

get/api/v1/admin/products/special-groups
Products/GetProductGroupsList admin paged

Returns the top-level special product groups.

Query parameters 3
searchstringSearches the group title.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 8
idintGroup id.
keystringThe group key. This is the value you use when opening a category.
titlestringThe group title in the current language.
routestringThe URL slug.
statusstringactive ya da inactive.
product_countintHow many products are in the group.
service_countintHow many services are in the group. This is the number to read before deleting.
created_atstringWhen it was created.
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 'https://panel.example.com/api/v1/admin/products/special-groups' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/special-groups', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/special-groups');
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()->GetProductGroupsList();
Response
{
  "data": [
    {
      "id": 5,
      "key": "special-5",
      "title": "SSL Certificates",
      "route": "ssl-certificates",
      "status": "active",
      "product_count": 8,
      "service_count": 40,
      "created_at": "2026-01-01 10:00:00"
    }
  ],
  "meta": { "total": 3, "page": 1, "limit": 25, "next_page": 0 }
}

Group Detail

get/api/v1/admin/products/special-groups/{id}
Products/GetProductGroup admin

Returns one group with all of its settings and language content.

Response fields data — 18
idintGroup id.
kindstringThe group type key.
kind_idintId of the parent group. Zero at top level.
parent_idintId of the parent category.
is_categoryboolWhether this row is a category or a top-level group.
group_keystring | nullThe group key on top-level groups.
statusstringactive ya da inactive.
visibilitystringvisible ya da invisible.
rankintThe display order.
icon_typestringfont ya da image.
iconstringThe icon class or the name of the uploaded image.
colorstringThe colour of the group card.
list_templateintId of the list template.
upgradingboolWhether upgrading inside the group is allowed.
seo_indexboolWhether search engines may index it.
enabled_payment_gatewaysarrayRestricts payment to these gateways.
disabled_payment_gatewaysarraySwitches these gateways off.
langsobjectA map from language code to a content object.
titlestringThe group title.
routestringThe URL slug.
sub_titlestringA sub-title.
contentstringThe group text.
seo_titlestringThe search title.
seo_keywordsstringThe search keywords.
seo_descriptionstringThe search description.
Errors 2
not_found404No such group.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/special-groups/5' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/special-groups/5', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/special-groups/5');
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()->GetProductGroup(['id' => 5]);

Creating a Group

post/api/v1/admin/products/special-groups
Products/CreateProductGroup admin 201

Opens a top-level special group.

Body 14
titleobjectrequiredA map from language code to title. The current language needs one.
sub_titleobjectA map from language code to sub-title.
contentobjectA map from language code to group text.
routeobjectA map from language code to slug. Left empty, it is built from the title.
statusstringactive ya da inactive.
hiddenboolHides the group from the storefront.
rankintThe display order.
icon_typestringfont ya da image.
iconstringThe icon class.
colorstringThe card colour. Six digits without the hash.
list_templateintId of the list template.
upgradingboolAllows upgrading inside the group.
seo_indexboolOpens it to search engines.
seo_titleobjectA map from language code to search title. Keywords and description are separate fields.
Response fields data
dataobjectThe group that was created, returned with 201. Same shape as the group detail schema.
Errors 4
title_required422There is no title in the current language.
route_exists422The slug clashes with an existing address.
create_failed422Creation was refused.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/special-groups' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"title":{"en":"SSL Certificates"},"status":"active","icon_type":"font","icon":"fa-lock","color":"3366ff"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/special-groups', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: { en: 'SSL Certificates' },
    status: 'active',
    icon_type: 'font',
    icon: 'fa-lock',
    color: '3366ff',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/special-groups');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'title'     => ['en' => 'SSL Certificates'],
        'status'    => 'active',
        'icon_type' => 'font',
        'icon'      => 'fa-lock',
        'color'     => '3366ff',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The new group's key is what you use when opening products and categories inside it.
$group = Api::Products()->CreateProductGroup([
    'title' => ['en' => 'SSL Certificates'],
]);

Api::Products()->CreateProduct([
    'type'     => 'special',
    'group_id' => $group['data']['id'],
    'name'     => 'Wildcard SSL',
]);

Updating a Group

patch/api/v1/admin/products/special-groups/{id}
Products/UpdateProductGroup admin partial-safe

Applies the fields you send and leaves the rest as they were.

Body 14
titleobjectrequiredA map from language code to title. The current language needs one.
sub_titleobjectA map from language code to sub-title.
contentobjectA map from language code to group text.
routeobjectA map from language code to slug. Left empty, it is built from the title.
statusstringactive ya da inactive.
hiddenboolHides the group from the storefront.
rankintThe display order.
icon_typestringfont ya da image.
iconstringThe icon class.
colorstringThe card colour. Six digits without the hash.
list_templateintId of the list template.
upgradingboolAllows upgrading inside the group.
seo_indexboolOpens it to search engines.
seo_titleobjectA map from language code to search title. Keywords and description are separate fields.
Response fields data
dataobjectThe group in its up-to-date state. Same shape as the group detail schema.
Errors 4
not_found404No such group.
route_exists422The slug clashes with an existing address.
update_failed422The update was refused.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/products/special-groups/5' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"inactive","color":"ff0000"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/special-groups/5', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: 'inactive', color: 'ff0000' }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The FAQ, columns and payment gateways are NOT edited here; they are preserved.
$response = Api::Products()->UpdateProductGroup([
    'id'     => 5,
    'status' => 'inactive',
]);

Deleting a Group

delete/api/v1/admin/products/special-groups/{id}
Products/DeleteProductGroup admin takes its contents

Deletes the group. Its products, sub-categories and images go with it.

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

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/special-groups/5');
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);
// The delete takes everything in the group - count the contents first.
$group = Api::Products()->GetProductGroupsList()['data'][0];

if ($group['product_count'] > 0 || $group['service_count'] > 0) {
    return;
}

Api::Products()->DeleteProductGroup(['id' => $group['id']]);

Uploading a Group Image

post/api/v1/admin/products/special-groups/{id}/image
Products/UploadGroupImage admin two slots

Uploads the group's icon or its header background.

Body 2
imagestringrequiredThe image. A base64 data URI or a link that can be fetched.
typestringWhich slot to fill: icon or header-background. Defaults to icon.
Response fields data — 2
typestringThe slot that was written: icon or header-background. Compare it with what you sent to catch a silent fallback.
urlstringPublic URL of the stored image. The file name is randomised.
Each slot holds one image: an upload replaces the previous one and deletes its file. header-background images are resized to the configured dimensions.
Errors 5
not_found404No such group.
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/special-groups/5/image' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"image":"https://cdn.example.com/ssl.png","type":"icon"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/special-groups/5/image', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    image: 'https://cdn.example.com/ssl.png',
    type: 'icon',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/special-groups/5/image');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'image' => 'https://cdn.example.com/ssl.png',
        'type'  => 'icon',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->UploadGroupImage([
    'id'    => 5,
    'image' => 'https://cdn.example.com/ssl.png',
    'type'  => 'icon',
]);

Deleting a Group Image

delete/api/v1/admin/products/special-groups/{id}/image
Products/DeleteProductGroupImage admin

Removes the group's icon or its header background.

Query parameters 1
typestringWhich slot to empty: icon or header-background. Defaults to icon.
Response fields data — 3
deletedboolWhether the delete succeeded.
idintGroup id.
typestringThe slot that was emptied.
Errors 2
not_found404No such group.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE -G 'https://panel.example.com/api/v1/admin/products/special-groups/5/image' \
  -H "Authorization: Bearer $API_KEY" \
  -d type=icon
const url = new URL('https://panel.example.com/api/v1/admin/products/special-groups/5/image');
url.searchParams.set('type', 'icon');

const res = await fetch(url, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/products/special-groups/5/image?' . http_build_query(['type' => 'icon']);

$ch = curl_init($url);
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()->DeleteProductGroupImage(['id' => 5], ['type' => 'icon']);

Pitfalls

Deleting takes the contents with it

Deleting a group also deletes the products, sub-categories and images inside it. The product_count and service_count fields on the list exist for exactly this; read both before you delete.

The rich fields are not edited here

The FAQ, the column layout, the operator notes and the payment gateway lists are not in the update body. They keep their current values, so leaving them out does not wipe them. They are managed from the panel's own screens.

The colour goes in without a hash

The colour field wants the six digits without a leading hash. Sending it with one can leave the value unreadable, so read the detail back after writing to see what was stored.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.