Completeness
Two routes, and they belong together: one tells you what a product should carry, the other how much of it it does.
The field list in one call
The catalog's shape is tenant data spread over six entities. Reading it by hand means five requests, a join, and a private attributes.type → input mapping in every client — and that mapping is exactly the part that must not be private, because the type list carries no fixed value set and integrators extend it.
GET /v1/products/attribute-schema answers one ready-to-render field list instead, ordered by group and then by the family's own ordering. It writes nothing.
curl "https://api.revenexx.com/v1/products/attribute-schema?family_code=power_tools" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Per field it reports:
- the attribute
code, its resolved label and itstype; - its
group, so a form can lay out sections; storage.path— the exact path insideattribute_valuesa value belongs at;from— the full fallback order a reader should follow;- the republished
validationkeys a client can act on:min,max,min_length,max_length,pattern,min_items,max_items; - whether the field is required, and why it may be read-only (an attribute that is a variant axis is read-only on the model).
An unknown type is mapped onto a text field rather than refusing to answer, so a tenant that models a fifth type still gets a renderable form.
Without a family it answers every attribute declared for the given entity_type / entity_ref — the shape of a reference entity's records or an asset family, which have attributes but no family:
curl "https://api.revenexx.com/v1/products/attribute-schema?entity_type=reference_entity&entity_ref=brand" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Completeness
POST /v1/products/{id}/completeness computes how much of what the product's family requires it actually carries, and stores the result in products.completeness.
curl -X POST "https://api.revenexx.com/v1/products/{id}/completeness" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
There is no body to send — everything it needs is the path id and what the catalog already holds.
The stored document:
| Key | Meaning |
|---|---|
required | How many attributes the family marks is_required. |
filled | How many of those carry a value in any bucket. |
ratio | filled / required, between 0 and 1. A family that requires nothing is 1, not undefined. |
missing | The codes with no value anywhere, sorted. |
computed_at | When this measurement was taken. |
It measures family_attributes.is_required and nothing else. Remember that required_channels of null or [] means required everywhere — see Data model.
A product with no family answers 400
A product with no family has no required attributes, so there is nothing to measure it against — and the app answers 400 rather than an invented 0 %.
Assign a family first, which computes and stores the completeness in the same call:
curl -X POST "https://api.revenexx.com/v1/products/{id}/family" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"family_code":"power_tools"}'
That is the step every family-driven surface waits on: without a family, completeness cannot be computed and the family's label_attribute never resolves.
Where the stored value shows up
products.completeness is a column, so it comes back on GET /v1/products/{id} and as a column in GET /v1/products/grid — a merchandiser's worklist is a grid sorted by it, not a route of its own. It is only as fresh as the last computation, so recompute after a bulk change.
Where to go next
- Data model — the six entities
attribute-schemaflattens. - Products — the four buckets
storage.pathaddresses. - Import and export — what to recompute after.