Associations
An association is a typed link from one product to another: an accessory, a spare part, a cross-sell, a component of a bundle.
| Entity | Base path | Is |
|---|---|---|
association_types | /v1/products/association_types | The kind of relation |
product_associations | /v1/products/product_associations | One link, source → target |
GET /v1/products/association_types also filters on active. active is true unless the entry is inactive (false). A list returns both unless you filter, so use ?active=true for a picker. Records that already carry an inactive entry keep it; a write cannot newly assign one (409 entry_inactive). active is also an optional boolean on POST and PUT of an association type, default true: send false to make it inactive, and anything that is not a boolean answers 400 invalid_active.
Association types
{
"code": "accessory",
"labels": { "en": "Accessories", "de": "Zubehör" },
"is_two_way": true,
"is_quantified": false
}
| Field | Meaning |
|---|---|
code | Unique per tenant. |
labels | What the relation is called in a product form, per locale key. Label keys follow the tenant's locale policy: de and en in a tenant that keeps text per language, as in the example above, de-DE and en-GB in one that keeps text per region. See Languages and locale codes. |
is_two_way | Declares the relation symmetric — an accessory of A is an accessory of B. |
is_quantified | Declares that a relation of this kind carries a quantity. |
is_two_way is a declaration a client reads, not behaviour the app performs. This app stores one row per direction and does not create the mirror for you. If you want the reverse link to exist as a row, write it.Product associations
{
"product_id": "…",
"association_type_id": "…",
"target_product_id": "…",
"position": 10
}
| Field | Meaning |
|---|---|
product_id | The product the relation starts at — the one whose detail page shows it. |
target_product_id | The product it points at — the accessory, the spare part, the cross-sell. |
association_type_id | Which kind of relation. |
position | Order in which targets are shown, ascending. |
quantity | How many of the target belong to the source. |
Quantified relations
quantity is the 4 in "this bundle contains 4 casters". It is only meaningful when the association type carries is_quantified — on an ordinary cross-sell it is null.
{ "code": "bundle_component", "is_quantified": true, "is_two_way": false }
{ "product_id": "<bundle>", "association_type_id": "…", "target_product_id": "<caster>", "quantity": 4 }
That pair is how a bundle or a bill of materials is expressed. Nothing in the catalog prices or explodes a bundle for you — the quantity is data for whatever does.
The products row also carries a quantified_associations jsonb column, kept for catalogs imported with one. The entities above are the surface this app reads and writes.
Create and list
| Operation | What it does |
|---|---|
POST /v1/products/association_types | Creates a type. code is required. A code already taken answers 409. |
POST /v1/products/product_associations | Creates a link. product_id, association_type_id and target_product_id are required; quantity and position are optional. The same three ids a second time answer 409, and so does an inactive association type (entry_inactive). |
GET /v1/products/product_associations | Lists links. Every column is an exact-match query parameter. |
Each create answers 201 with the stored row.
Read, change or delete one entry
GET returns the whole row as stored. PUT is a partial patch: only the named columns change, and a body that names no column answers 400. On an association type, an active that is not true or false also answers 400 (invalid_active), and a locale key in labels that is not one of the tenant's write keys answers 422. DELETE is a hard delete, with no trash and no restore. An unknown id answers 404, and so does another tenant's id.
| Operation | What it does |
|---|---|
GET /v1/products/association_types/{id} | Reads one association type. |
PUT /v1/products/association_types/{id} | Changes the named columns. A code already taken answers 409. |
DELETE /v1/products/association_types/{id} | Deletes the type, and with it every product association of that type (association_type_id). |
GET /v1/products/product_associations/{id} | Reads one link. |
PUT /v1/products/product_associations/{id} | Changes the named columns. A clash on product_id + association_type_id + target_product_id answers 409, and so does pointing it at an inactive association type (entry_inactive). |
DELETE /v1/products/product_associations/{id} | Deletes the link. Nothing else changes. |
Where to go next
- Products — the rows on both ends of a link.
- Categories — the other way products are grouped.
- Order lists — reusable position collections, which are not associations.