What Is on Sale

7 views Markdown

The three endpoints giving the products on sale, their categories and the order schema.

Overview

These three endpoints show what a customer can buy. What the shop window holds is what appears here: a product that is closed, hidden or in a shut group never shows at all.

The prices come in the account's wallet currency. The figure listed can be compared with the balance straight away, and ordering brings no surprise.

The detail endpoint gives more than a catalogue record: it is the schema of the order body. The cycles, add-ons, questions and the domain axis are read from it.

Reference

Listing the Products

get/api/v1/client/products
Products/GetProducts the wallet currency

Returns the products that can be ordered, in shop-window order.

Query 5
pageintWhich page.
limitintRows per page. 100 at the most.
typestringOne product type. An unknown type is refused and the valid list comes in the answer's detail.
categoryintThe category number. It matches the main category and the extra assignments alike.
searchstringSearches the product title.
Response fields data[] — 8 + meta — 5
idintThe product id.
typestringThe product type.
titlestringThe product name.
taglinestringA short line about it.
categoryobjectThe main category. It carries a number and a name, and comes empty on a product with none.
in_stockboolWhether it can be ordered. It closes only with stock tracking on and the counter at zero.
stockintThe stock left. Empty means it is not tracked.
pricingobject[]One price row per billing cycle.
totalintHow many products there are. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
currencystringThe currency every price is in. It is the account's wallet currency.
Errors 2
type_invalid422The product type is unknown or that group is closed.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/products?type=hosting' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/products?type=hosting', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data, meta } = await res.json();
renderPrices(data, meta.currency);
$ch = curl_init('https://panel.example.com/api/v1/client/products?type=hosting');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The prices come in the WALLET currency: compare them with the balance straight and convert nothing.
$r = Kernel::internal('client:Products/GetProducts', ['owner_id' => $uid, 'type' => 'hosting']);
$cur = $r['meta']['currency'];

Listing the Categories

get/api/v1/client/products/categories
Products/GetProductCategories no paging

Returns the product categories on show as one flat list.

Query 1
typestringThe categories of this type alone. An unknown value gives an empty list rather than an error.
Response fields data[] — 4 + meta — 1
idintThe category number. This is what goes into the product listing's category filter.
parentintThe parent category. Empty means it sits at the top.
typestringThe category's product type.
namestringThe category name.
totalintHow many categories there are. It comes back under meta.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/products/categories' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/products/categories', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const roots = data.filter((c) => c.parent === null);
$ch = curl_init('https://panel.example.com/api/v1/client/products/categories');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The list is FLAT: build the tree from the parent field yourself, since the endpoint gives none.
$rows = Kernel::internal('client:Products/GetProductCategories', ['owner_id' => $uid])['data'];
$tree = [];
foreach ($rows as $c) $tree[$c['parent'] ?? 0][] = $c;

The Product and Its Order Schema

get/api/v1/client/products/{id}
Products/GetProduct the order input

Returns a product with everything needed to build an order body.

Response fields data — 14
idintThe product id.
typestringThe product type.
titlestringThe product name.
taglinestringA short line about it.
descriptionstringThe full description. It comes with the markup the shop shows.
categoryobjectThe main category.
in_stockboolWhether it can be ordered.
stockintThe stock left.
quantityobjectThe quantity rule. It carries a permission and a mode: one unit, each unit its own service, or a quantity scaling inside one service.
order_limit_per_userintHow many services one account may hold. Empty means there is no limit.
pricingobject[]One price row per billing cycle.
addonsobject[]The add-ons an order can pick. Each carries an id, a name, a description, an input type, whether it is required, its choices and its own questions.
requirementsobject[]The fields an order has to fill. Each carries an id, a name, an input type and whether it is required; a choice type adds its options and a file type adds the extensions allowed and any size limit.
metricsobject[]The meters charged by use. Each carries an id, a type, a label, a unit, a charging scheme, what is included, a ceiling and the tiered prices.
domainobjectThe domain axis. By its mode it carries a chooser list, subdomain roots or the licence fields.
Errors 2
not_found404The product is absent, closed, hidden or its group is shut. The four are deliberately not told apart.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/products/36' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/products/${id}`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const must = data.requirements.filter((r) => r.required);
const cycles = data.pricing.map((p) => p.cycle);
$ch = curl_init('https://panel.example.com/api/v1/client/products/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A NEW domain cannot be ordered here: registering and transferring happen on the Domains endpoints.
$pr = Kernel::internal('client:Products/GetProduct', ['owner_id' => $uid, 'id' => $id])['data'];
$viaDomains = ($pr['domain']['register_transfer'] ?? '') === 'via_domains_endpoints';

Pitfalls

A new domain is not ordered from here

The domain axis in the product detail covers using a domain you already own or picking a free subdomain. Registering a new domain or transferring one belongs to the domain endpoints. Mixing them up sends you looking for an order field that does not exist.

The price listed is the promotional one

Where a cycle carries a promotion the price field holds the promotional amount and the promotion flag comes back true. Applying a discount on top of the listing takes it down twice. This is also what the order engine charges, so the listing and the basket never disagree.

A product not found can mean four things

The detail endpoint meets an unknown, closed, hidden and group-shut product with the same answer. That is deliberate: the answer never leaks whether the product exists. When one is missing from the listing the API will not say why, and the operator has to be asked.

The quantity mode means three different orders

The quantity rule sits in one of three modes: one unit per order, a separate service for each unit, or a quantity scaling inside one service. Ordering ten in the second mode brings ten services into being, and in the third one service at ten times the price. Read the mode before building the interface.

The category list is flat and not a tree

The categories come back flat and the hierarchy is built from the parent field. There is no paging either: the whole set comes every time. A chosen category goes into the product listing as a filter and does not take its children in by itself.

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.