Associations

Association types and product associations — cross-sells, accessories, spare parts and quantified relations such as bundles and bills of materials.

An association is a typed link from one product to another: an accessory, a spare part, a cross-sell, a component of a bundle.

EntityBase pathIs
association_types/v1/products/association_typesThe kind of relation
product_associations/v1/products/product_associationsOne 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

Association type
{
  "code": "accessory",
  "labels": { "en": "Accessories", "de": "Zubehör" },
  "is_two_way": true,
  "is_quantified": false
}
FieldMeaning
codeUnique per tenant.
labelsWhat 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_wayDeclares the relation symmetric — an accessory of A is an accessory of B.
is_quantifiedDeclares 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

Association
{
  "product_id": "…",
  "association_type_id": "…",
  "target_product_id": "…",
  "position": 10
}
FieldMeaning
product_idThe product the relation starts at — the one whose detail page shows it.
target_product_idThe product it points at — the accessory, the spare part, the cross-sell.
association_type_idWhich kind of relation.
positionOrder in which targets are shown, ascending.
quantityHow 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.

Association type
{ "code": "bundle_component", "is_quantified": true, "is_two_way": false }
Association
{ "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

OperationWhat it does
POST /v1/products/association_typesCreates a type. code is required. A code already taken answers 409.
POST /v1/products/product_associationsCreates 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_associationsLists 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.

OperationWhat 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.
Was this page helpful?