Vizen Shop

Catalogue

/docs/skill/vizen-catalogcurrentEN

1. Filters need the filter. prefix — this is not cosmetic

GET /products?filter.category_id=168&page.limit=24

A parameter written without the prefix is swallowed silently: ?category_id=168 answers 200 and returns the entire catalogue. No error, just the wrong data — and a storefront built from it looks plausible.

FilterMeaning
filter.category_idproducts of a section
filter.idsseveral products at once
filter.querysearch by name
filter.is_publishedpublished only
filter.min_price / filter.max_priceprice range
page.number / page.limit, sort.field / sort.orderpaging and order

The check that catches this class of bug: compare total before and after adding a filter. Unchanged means the parameter was not understood.

2. Shapes that surprise

  • result for lists is an object — {items, total, currentPage}, not an array;
  • ids come back as strings from some endpoints and numbers from others;
  • category.parent_id is a string while category.id is a number;
  • a category is also a page: type is product, page or news.

3. Images

Files come back as absolute storage URLs. The resizer serves /w/{width}[/webp]{path} where path is that URL minus the storage host.

In your own markup you do not build these paths: write a plain <img src> and the server rewrites it, adding srcset. Do set sizes yourself — see vizen-own-markup.

⚠️ HEAD on a resizer URL answers 404 while GET answers 200. Do not conclude the resizer is broken.

4. What the data may not tell you

A shop can contain a published, promising, empty category — for example one named "Sale" — while the platform has no promotions entity at all (GET /promotions[], no promo field on any product). An empty result does not distinguish "this shop has no sales" from "there is no such concept".

When a task depends on a data property you cannot verify exists, ask the owner rather than curating something by hand and calling it the thing they asked for.

5. Galleries are a resource of their own

A gallery is not a field on a product. It lives at /galleries with its own CRUD (GET, POST, PUT /galleries/{id}, DELETE, GET /galleries/{id}/usage), and products, categories and articles only *link* to it:

POST /galleries    { "item": { "name": "Blue", "items": [ { "file": { "id": "<file_id>" } } ] } }
PUT  /products/808 { "item": { "gallery_ids": ["31"], "gallery_ids_replace": true } }
  • gallery_ids follows the categories_replace pattern: a non-empty list replaces, an empty list without the flag means "do not touch", an empty list with the flag unlinks everything. Ids are strings (int64 in JSON). A gallery's own items follows the same rule with its own flag: clearing needs items: [] plus items_replace: true;
  • galleries may be longer than gallery_ids: a gallery a variant set points at is appended even when the product does not link it, and stays out of gallery_ids on purpose. Look content up by id, not by position;
  • one gallery can sit on several objects — that is the model, not a bug, and editing it changes every card that shows it;
  • a variant set links to any live gallery of the shop (items[].gallery_id in PUT /products/{id}/variants) — this is how you reuse the gallery of another product. Its image_id is a separate field: chip and cart row, not the slider;
  • the server derives the item's role (image | video | audio) from the file type; you set "html" by hand and nothing else. A kind that disagrees with the file is refused with GALLERY_ITEM_KIND_MISMATCH, never quietly fixed;
  • a gallery in use cannot be deleted: GALLERY_IN_USE. Unlink first, check GET /galleries/{id}/usage, then delete;
  • lists (GET /products, GET /categories) never carry galleries — only preview, which falls back to the first image of the first gallery (preview_auto: true marks that fallback). Read galleries from card endpoints.

Ask the card for fewer frames. A product shot in 360° for a dozen colours answers with a dozen galleries of ~36 frames — measured 159 KB, of which 138 KB was galleries nobody looked at. Both card endpoints take galleries=:

GET /products/by-slug/{slug}?company_id=14&galleries=cover              # every gallery = its cover only
GET /products/{id}?galleries=active&variant=rs7-blue                    # shown gallery in full, rest as covers

variant is the set's sku (its id as a string when sku is empty) — the same key the storefront puts in ?v=. Omit it and the server takes the first active set, then the product's first linked gallery. Without the parameter the answer is exactly what it always was. Read frame counts from items_total (items before the projection), never from items.length under a projection; fill a cover in with GET /galleries/{id}, which always returns everything. An unknown value answers GALLERIES_PROJECTION_UNKNOWN instead of silently ignoring you.

Limits: ≤24 items per gallery, name 1..64 characters, ≤10 galleries per object, item html ≤64 KB (GALLERY_ITEM_HTML_TOO_LONG). GET /galleries is paged — ?page.number=&page.limit= (1..100), first 50 without params; total and currentPage sit next to result[]. Full recipe: GET /docs/catalog-import §7; the area page: GET /docs/catalogue §7.

6. Writing

Prices are whole units of the shop's currency; the currency cannot be changed with a token, and a mismatch is rejected. Import guide: GET /docs/catalog-import.

Read every write back and compare the fields you sent: a 200 proves only that the request parsed.

Prices, discounts and sets

The price a shopper sees is computed, not stored. There is no discounted price column: the server runs every card through the same engine that prices the cart. That single fact decides everything else here.

Consequences you cannot design around:

  • never recompute a discount client-side. Your number will disagree with the cart on the first product two rules touch, and the shopper trusts the cart;
  • a product's price already includes per-item rules — price, old_price and promotion_name come back priced;
  • rules behind a promo code never reach a product card. A storefront must not advertise a price that does not exist without the code, and a personal price in a shared page would leak someone else's coupon;
  • [] from GET /promotions means "this shop has no rules", not "you lack access" — a missing scope answers 403 PAT_SCOPE_MISSING. Do not treat them alike.

Where to look:

You needAsk
what is on sale right now, rankedGET /promotions/landscape — the server ranks it
the rules themselvesGET /promotions
the machine list: kinds, classes, refusal codes, roundingGET /docs/promotions.json
how any of it actually worksGET /docs/promotions — the area description

Do not rebuild the ladder (item → order → key) yourself to find discounted products. landscape exists precisely so you do not have to, and it reports scanned_products and truncated so a ceiling never reads as "nothing else is on sale".

For printing "cheaper in this set" on a card, see vizen-own-markup — the numbers arrive as fields with ready strings beside them, because substitutions have no expressions.

Исходник: https://api.vizen.shop/docs/skill/vizen-catalog

Работает в демо-режиме