Products

The product row on revenexx — sku, kind, parent_id, the four-bucket attribute document, the flattened grid, and the batch resolvers other apps call.

The products entity is the catalog itself. It is deliberately narrow: five meaningful columns plus one jsonb document that holds everything the merchant modelled.

Request
curl "https://api.revenexx.com/v1/products?limit=20" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..."

The row

ColumnMeaning
idServer-generated uuid. A client never sends one — it reads one back and uses it in later paths.
skuThe merchant's own article number. Unique per tenant, and the value every integration joins on. A second row with the same sku answers 409 with "code": "unique_violation".
kindWhere the row sits in the variant hierarchy: simple, model or variant.
parent_idThe model a variant belongs to. Only a variant carries one. Deleting the model leaves its variants with a null parent rather than deleting them.
family_idWhich set of attributes this product has.
family_variant_idWhich variant structure of that family it follows.
enabledWhether the product is live.
tax_classThe tax class key the prices app resolves a VAT rate from. Free text here — the vocabulary belongs to the app that prices.
attribute_valuesEvery modelled value, in one jsonb document. See below.
labelThe resolved display name, maintained by the database. Read-only.
completenessWhat POST /{id}/completeness last measured.

sku is the only column a create cannot omit. Two defaults are applied by the app rather than by the column: enabled comes from the tenant's new_products_enabled_by_default setting, so an import cannot publish twenty thousand unfinished products the moment it lands, and a product naming no family gets the tenant's default_product_family. An explicit value in the body wins over both.

The variant hierarchy

  • simple stands on its own.
  • model carries the values its variants share and is never sold itself.
  • variant carries the axis values and points at its model through parent_id.

Which attributes are the axes is declared per family by family_variants. An attribute named as an axis is read-only on the model and set on each variant.

attribute_values — four buckets

A product's properties are not columns. They are rows in attributes, selected per family by family_attributes, and their values live under the attribute code inside one of four buckets. The attribute's own localizable and scopable flags decide which:

attribute_values
{
  "common": { "net_weight": 2.4, "colour": "black" },
  "locale_specific": { "de_DE": { "name": "Akku-Bohrschrauber" } },
  "channel_specific": { "b2b": { "minimum_order_quantity": 6 } },
  "channel_locale_specific": { "b2b": { "de_DE": { "description": "" } } }
}
BucketWritten when
commonNeither localizable nor scopable — one value, full stop.
locale_specificlocalizable — one value per language tag.
channel_specificscopable — one value per channel.
channel_locale_specificBoth — one value per channel and language tag.

Reading falls back. A reader takes the most specific bucket carrying the code, then locale, then channel, then common. common is always consulted last and always consulted, because early imports wrote everything there regardless of an attribute's flags — a reader that skips it reports an imported catalog as empty.

Do not re-derive that chain. GET /v1/products/attribute-schema answers, per field, the exact path a value belongs at and the full fallback order.

The value itself is whatever the attribute's type implies: a string, a number, a boolean, an option code for a select (never its label), a list of codes for a multi-select, {"amount": …, "unit": …} for a measure, a list of {"amount": …, "currency": …} for a price, an asset code for media.

{} is a normal state — a product nobody has enriched yet.

label is generated, and tells you when it is missing

label is maintained by the database so a grid of twenty thousand rows can sort and filter on a name without a join. It resolves the first of attribute_values.common.name, common.label, the de / en / de_DE / en_US locale buckets, common.manufacturer_aid, and finally the SKU.

So a value is always present — and a label equal to the SKU means the catalog holds no name for this product. Show it as a missing name, not as a name.

The grid

GET /v1/products answers SKUs and a jsonb blob. GET /v1/products/grid answers the list a person can scan: each row already flattened to its resolved display name, where that name came from, its family code, its stored completeness, and the value of every attribute marked usable_in_grid — no join, no second call.

Request
curl "https://api.revenexx.com/v1/products/grid?q=bohr&enabled=true&limit=50" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..."

Two things to know:

  • It filters on q, kind, enabled and family_id and nothing else. An unknown query parameter is refused with 400 rather than silently dropped. q is a case-insensitive substring of the stored label, which falls back to the SKU, so one box finds a product by either.
  • The response's filters array reports the attributes marked is_filterable — that is what a filter bar should offer. It is not a query surface: filtering by attribute value is not offered by this API, because the values sit inside a four-bucket jsonb document behind a fallback chain.

Soft-deleted products are excluded from the grid, unlike GET /v1/products.

Two resolvers for other apps

POST /v1/products/batch answers id, sku, tax_class and the resolved display name for a list of ids and/or SKUs in one call. It exists for the app on the other side of a product reference — the prices app holds SKUs and needs a tax class, a feed builder holds ids and needs names — so neither pages through the catalog nor fires a request per line.

Request
curl -X POST https://api.revenexx.com/v1/products/batch \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"skus":["ACME-4711-BLK","ACME-4711-RED"]}'

It answers what it found: an identifier naming nothing is simply absent from items rather than an error. Compare what you sent with what came back if a miss matters.

POST /v1/products/labels answers what is this product called for up to 500 products at once, and reports where each name was found. source: "sku" means the catalog holds no name for it.

Writes

RouteDoes
POST /v1/productsCreate one product. Answers 201 with the stored row.
PUT /v1/products/{id}Update.
DELETE /v1/products/{id}Delete.
POST /v1/products/{id}/familyAssign a family by family_id or family_code, and recompute completeness in the same call.
POST /v1/products/{id}/categoriesFile the product into a category by hand. The membership is always source: "manual", so a rule recompute never deletes or shadows it.

Where to go next

  • Data model — the attributes, families and variant structures this row points at.
  • Completenessattribute-schema and the completeness measure.
  • Categories — the tree and rule-driven membership.
  • Import and export — loading a catalog in volume.
Was this page helpful?