Data model
Adding a property to a product on revenexx is not a migration. It is a row in attributes and a row in family_attributes. Six entities describe the whole shape of a tenant's catalog, and every one of them is ordinary tenant data a merchant can change.
| Entity | Base path | Answers |
|---|---|---|
attribute_groups | /v1/products/attribute_groups | Which section of a form a field appears in |
attributes | /v1/products/attributes | What properties exist at all |
attribute_options | /v1/products/attribute_options | The permitted values of a select |
families | /v1/products/families | Which set of attributes a product has |
family_attributes | /v1/products/family_attributes | The link, plus per-family required and order |
family_variants | /v1/products/family_variants | How a family splits into variants |
measurement_families | /v1/products/measurement_families | The units a measure attribute offers |
The active flag works the same on these six entities (attribute_groups, attributes, attribute_options, families, family_variants and measurement_families):
- Write.
activeis an optional boolean onPOSTandPUT, defaulttrue. Sendfalseto make an entry inactive. Anything that is not a boolean answers400 invalid_active. - Read. A list returns active and inactive entries unless you filter. Use
?active=truefor a picker. - Existing records. Records that already carry an inactive entry keep it. A write cannot newly assign one (
409 entry_inactive).
Attributes
curl "https://api.revenexx.com/v1/products/attributes?limit=100" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
| Field | Meaning |
|---|---|
code | Stable identifier, and the key the value is stored under inside attribute_values. Also how a category rule addresses it, as attribute:<code>. |
type | Which editor the value asks for — text, select, metric, price, asset_collection, reference_entity, … |
localizable | One value per locale, under locale_specific.<locale>.<code>. |
scopable | One value per channel, under channel_specific.<channel>.<code>. Both flags together means channel_locale_specific. |
entity_type | Which kind of record carries it: product, reference_entity, asset, category. |
entity_ref | Narrows entity_type to one reference entity or asset family, by code. Null for a plain product attribute. |
group_id | The attribute_groups row it is filed under — the form section. Null renders after every named section. |
position | Order inside the group. A family may override it. |
labels | The field label per locale key, under the tenant's write key. A reader walks the policy's read order and shows the code when nothing is found. |
validation | Limits, as a flat object. The seven keys a client can act on: min, max, min_length, max_length, pattern, min_items, max_items. |
config | Type-specific settings. The app reads units (a measure's unit list) and reference_entity (which entity a reference attribute draws options from). |
is_filterable | Offer this attribute as a filter in a product list. GET /v1/products/grid reports exactly these in its filters array. |
usable_in_grid | Show it as a column in the grid. GET /v1/products/grid returns a column definition and a per-row value for exactly these. |
is_unique | Declares the value identifies the product — an EAN, a manufacturer part number. Metadata only: no database index enforces it, because the value lives inside jsonb. |
type and entity_type deliberately carry no fixed value list. A tenant or integrator can add a type; GET /v1/products/attribute-schema maps an unknown one onto a text field rather than refusing to answer.
Attribute groups
A group is a section of the product form, not just a label. code is what a resolved field carries as its group, labels is the heading a person sees, and position orders the sections ascending.
Attribute options
The permitted values of a select or multi-select.
{
"attribute_id": "…",
"code": "matte_black",
"labels": { "en": "Matte black", "de": "Mattschwarz" },
"position": 20,
"swatch": { "hex": "#1b1b1b" }
}
Label keys follow the tenant's locale policy: de and en in a tenant that keeps text per language, as in these examples, de-DE and en-GB in one that keeps text per region. See Languages and locale codes.
code is what gets stored in the product's attribute_values — never the label. Two tenants may label the same code differently. Deleting the attribute deletes its options with it. Options that tie on position keep whatever order the database returned, so give every option a position if the order matters.
Families
A family answers which attributes does a product of this kind have.
| Field | Meaning |
|---|---|
code | Stable identifier, and what GET /v1/products/attribute-schema?family_code= resolves. |
labels | What the family is called, per locale key. |
label_attribute | Which attribute code carries the display name of a product in this family. Null falls back to the default_label_attribute setting, then to the conventional name. |
image_attribute | Which attribute code carries the main image — what a grid thumbnail and a picker read. |
A product's name being per-family is the reason POST /v1/products/labels exists: no plain read can answer what a product is called.
Family attributes
One row per (family, attribute). This is the link that makes an attribute part of a family's form.
| Field | Meaning |
|---|---|
is_required | The attribute has to carry a value for a product of this family to count as complete. POST /v1/products/{id}/completeness measures exactly these and nothing else. |
position | The family's own ordering, overriding the attribute's default. |
required_channels | Narrows is_required to named channels. |
required_channels: null or [] means required everywhere, not nowhere. That is how every required link in the wild is stored. Reading an empty list as "nowhere" reports a fully configured family as demanding nothing.Deleting either side deletes the link.
Family variants
How a family splits — not which product splits. A family may carry several structures, and a product names the one it follows through family_variant_id.
axes names the attribute codes the split happens on, and two shapes are in the wild; both are read:
{ "code": "shoes_by_colour_size", "axes": ["colour", "size"] }
{
"code": "shoes_by_colour_size",
"axes": [
{ "level": 1, "axes": ["colour"] },
{ "level": 2, "axes": ["size"] }
]
}
An attribute named as an axis is set on each variant, not on the model. The attribute schema marks it read-only when you ask with kind=model, which is a hint to a form and not a rule of the API: a write of an axis value to a model is accepted.
Measurement families
The unit set a measure attribute offers.
{
"code": "weight",
"standard_unit": "KILOGRAM",
"units": [
{ "code": "KILOGRAM", "symbol": "kg", "convert_factor": 1 },
{ "code": "GRAM", "symbol": "g", "convert_factor": 0.001 }
],
"labels": { "en": "Weight" }
}
convert_factor multiplies a value into standard_unit, so a gram is 0.001 kilograms. Every value is converted to the standard unit before it is compared or sorted. symbol is what a form prints next to the number.
List and create
Every list is paged with limit and offset, and reports page.total. Every column of the entity is an exact-match query parameter, order sorts by one column, and a query key that is not a column is refused with 400 (unknown_filter). The API reference still says that such a key is dropped; the API refuses it, as described here.
| Operation | What it does |
|---|---|
GET /v1/products/attributes | Lists attributes. |
POST /v1/products/attributes | Creates an attribute. code and type are required. A clash on entity_type + entity_ref + code answers 409; two rows with an unset entity_ref never clash. |
GET /v1/products/attribute_groups | Lists the form sections. |
POST /v1/products/attribute_groups | Creates a section. code is required. A code already taken answers 409. |
GET /v1/products/attribute_options | Lists attribute options. |
POST /v1/products/attribute_options | Creates an option. attribute_id and code are required. A clash on attribute_id + code answers 409. |
GET /v1/products/families | Lists families. |
POST /v1/products/families | Creates a family. code is required. A code already taken answers 409. |
GET /v1/products/family_attributes | Lists the family-attribute links. |
POST /v1/products/family_attributes | Links an attribute to a family. family_id and attribute_id are required. A clash on family_id + attribute_id answers 409. |
GET /v1/products/family_variants | Lists family variants. |
POST /v1/products/family_variants | Creates a family variant. family_id and code are required. A code already taken answers 409. |
GET /v1/products/measurement_families | Lists measurement families. |
POST /v1/products/measurement_families | Creates a measurement family. code and standard_unit are required. A code already taken answers 409. |
Every create answers 201 with the stored row, including its id. A body whose active is not true or false answers 400 (invalid_active), on every entity here but family_attributes.
Read, change or delete one entry
Attribute groups, attributes, attribute options, family attributes, measurement families, families and family variants each have three routes on {id}:
GETreturns the whole row as stored.PUTis a partial patch: it names only the columns to change, and every column it leaves out keeps its value. A body that names no column answers400, and so does anactivethat is nottrueorfalse(invalid_active, on every entity here butfamily_attributes). On an attribute, a family attribute or a family, pointing the row at an inactive entry it did not point at before answers409 entry_inactive.- Locale keys. On every entity here but
family_attributes, a locale key inlabelsthat is not one of the tenant's write keys answers422(unknown_locale,ambiguous_locale,invalid_locale, orduplicate_localewhen two spellings of one key carry different values). DELETEis a hard delete: the row is gone, with no trash and no restore. To take an entry out of use and keep it, setactivetofalseinstead.- Unknown id. An unknown id answers
404, and so does another tenant's id.
The table below lists every cascade for each entity: a delete changes nothing beyond what its row names. Products store attribute and option codes inside attribute_values, not ids, so a value already stored under a deleted attribute or option stays as it is.
| Operation | What it does |
|---|---|
GET /v1/products/attribute_groups/{id} | Reads one section. |
PUT /v1/products/attribute_groups/{id} | Changes the named columns. A code already taken answers 409. |
DELETE /v1/products/attribute_groups/{id} | Deletes the section. attributes.group_id is set to null; the attributes stay. |
GET /v1/products/attributes/{id} | Reads one attribute. |
PUT /v1/products/attributes/{id} | Changes the named columns. A clash on entity_type + entity_ref + code answers 409; two rows with an unset entity_ref never clash. |
DELETE /v1/products/attributes/{id} | Deletes the attribute, and with it its attribute options and family attributes. |
GET /v1/products/attribute_options/{id} | Reads one option. |
PUT /v1/products/attribute_options/{id} | Changes the named columns. A clash on attribute_id + code answers 409. |
DELETE /v1/products/attribute_options/{id} | Deletes the option. Nothing else changes. |
GET /v1/products/family_attributes/{id} | Reads one family-attribute link. |
PUT /v1/products/family_attributes/{id} | Changes the named columns. A clash on family_id + attribute_id answers 409. |
DELETE /v1/products/family_attributes/{id} | Deletes the link. Nothing else changes. |
GET /v1/products/measurement_families/{id} | Reads one measurement family. |
PUT /v1/products/measurement_families/{id} | Changes the named columns. A code already taken answers 409. |
DELETE /v1/products/measurement_families/{id} | Deletes the family. Nothing else changes. |
GET /v1/products/families/{id} | Reads one family. |
PUT /v1/products/families/{id} | Changes the named columns. A code already taken answers 409. |
DELETE /v1/products/families/{id} | Deletes the family, with its family attributes and family variants. Products of the family stay, with family_id set to null. |
GET /v1/products/family_variants/{id} | Reads one family variant. |
PUT /v1/products/family_variants/{id} | Changes the named columns. A code already taken answers 409. |
DELETE /v1/products/family_variants/{id} | Deletes the family variant. Products that followed it stay, with family_variant_id set to null. |
Seed the base model
curl -X POST https://api.revenexx.com/v1/products/defaults \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
POST /v1/products/defaults creates the smallest catalog model a tenant can work with, for a tenant that has none: the attributes name (text, localizable), description (textarea, localizable) and images (asset_collection), and the family standard carrying all three, with name as its label attribute and required, and images as its image attribute. Every attribute and family the seed writes carries metadata.seeded_by: "products.defaults", and that mark is how a run tells its own rows from the tenant's. The model is created without a market.
- Any attribute or family without the mark (one the tenant created): the answer is
seeded: falsewithreason: not_empty, and nothing is written. - No attribute or family at all, or only marked ones: the run creates whatever of the model is missing. So a run that stopped half-way is finished by the next one.
- The whole model already in place: nothing is written, and the answer is
seeded: falsewithreason: already_seeded. That makes the call idempotent.
The answer lists the attribute codes the run created, the family code if it created the family, and how many attributes it assigned.
Read the shape in one call
Six entities is five reads and a join. Do not do that — GET /v1/products/attribute-schema answers one ready-to-render field list, ordered by group then by the family's own ordering, with the storage path and fallback order per field. See Completeness.
Where to go next
- Model a catalog with attributes, families and variants — the walkthrough: decide the model, build it over the API and repair a flat import.
- Products — the row these definitions shape.
- Completeness —
attribute-schemaand the completeness measure. - References — where a
reference_entityattribute draws its values from. - Assets — asset families and their attributes.