Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.
A catalog object is the canonical record Luigi’s Box AI indexes after mapping a source feed or API request. Source field names and catalog field names are separate concerns.
Registered fields
Section titled “Registered fields”Fields beginning with @ come from a closed registry. Customers cannot define arbitrary @ fields, and an unrecognized @ field is rejected during ingestion.
The currently supported registered fields are:
| Field | Authority | Requirement | Purpose |
|---|---|---|---|
@id | Client | Required | Stable typed identifier, such as product/123. |
@type | Client | Required | Object type. A product uses exactly product. |
@title | Client | Required | Human-readable object name. |
@category | Client | Optional | Product-to-category relationship using typed category IDs, such as category/cat-t-shirts. |
@brand | Client | Optional | Product-to-brand relationship using a typed brand ID. |
@parent | Client | Optional | The object’s immediate parent, as a typed ID. Builds category trees — see Hierarchies. |
@group_id | Client | Optional | Non-empty string shared by all products in one variant group; omitted for standalone products. |
@group_primary | Client | Optional | Boolean set to true for the primary product in a group; omitted together with @group_id for standalone products. |
@updated_at | System | System-managed | Last update timestamp maintained by Luigi’s Box AI. Do not send this field in client feeds or API requests. |
A complete product object can combine registered @ fields with ordinary public attributes:
{ "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "@category": ["category/cat-t-shirts"], "@brand": "brand/northwear", "@group_id": "northwear-t-shirt-1001", "@group_primary": true, "url": "https://example.com/products/blue-cotton-t-shirt", "image_url": "https://cdn.example.com/products/sku-1001.jpg", "price": 29.90, "list_price": 39.90, "availability": 1}Canonical object examples are shown as JSON because this is the post-mapping representation. XML, JSON, JSON Lines, and CSV are source feed formats; see the feed type references for examples.
Types and IDs
Section titled “Types and IDs”Use the configured type exactly. A product object uses product.
The source value used as identity must include the object type prefix:
product/123For a product, the prefix must be product/ and must match the configured @type:
{ "@id": "product/123"}For example, item/123 is rejected when the object type is product; it is not silently rewritten.
Relationships
Section titled “Relationships”Two registered fields link one object to another. Both hold the target’s typed identity — never a copy of the target itself.
@category connects a product to categories by typed category ID:
{ "@id": "product/123", "@type": "product", "@title": "Blue Cotton T-Shirt", "@category": [ "category/shirts", "category/summer" ]}The category objects use the corresponding IDs:
{ "@id": "category/shirts", "@type": "category", "@title": "Shirts"}The target category does not need to exist when the product is ingested. Product and category feeds can therefore be processed independently.
@brand connects a product to a brand by typed brand ID:
{ "@id": "product/123", "@type": "product", "@title": "Blue Cotton T-Shirt", "@brand": "brand/northwear"}The brand object uses the corresponding ID:
{ "@id": "brand/northwear", "@type": "brand", "@title": "Northwear"}The target brand does not need to exist when the product is ingested. Product and brand feeds can therefore be processed independently.
Hierarchies
Section titled “Hierarchies”@parent expresses a category tree: each category names its immediate parent, and
Luigi’s Box assembles the tree from those edges. Do not send a whole path on a category.
{ "@id": "category/clothing", "@type": "category", "@title": "Clothing"}A child names its parent:
{ "@id": "category/shirts", "@type": "category", "@title": "Shirts", "@parent": ["category/clothing"]}And a grandchild names only its own parent, not the whole chain:
{ "@id": "category/dress-shirts", "@type": "category", "@title": "Dress shirts", "@parent": ["category/shirts"]}The rules:
@parentis an array, holding one typed identity — the object’s immediate parent.- A root category omits it, or sends an empty array.
- The parent need not exist yet. Like
@categoryand@brand, the link is stored as sent; ordering between feeds does not matter. - Send one edge per object. Naming a grandparent as well has no defined resolution.
- It is not restricted to categories — any object type may carry it.
Variant groups
Section titled “Variant groups”Every variant is a complete product with its own stable @id. Products in the same
variant group carry the same string in @group_id; the value is an opaque group
identifier and does not need to be a UUID or the @id of another product.
Use @group_primary: true for the primary product in a group. Both fields are
optional. When a group has no primary product, set it to false on all members or
omit it. A standalone product omits
both fields. Catalog ingestion preserves the optional @ fields exactly as sent; it
does not add grouping defaults to the client-owned slots. In a partial update, omitting
a field leaves its stored value unchanged; send false explicitly to demote an existing
primary product. A partial update can change @group_primary without restating an
unchanged group ID. Send @group_id: null to remove a product from its current group;
its derived group ID then falls back to its own identity and its derived primary flag
becomes false.
Catalog ingestion validates each product independently and does not enforce uniqueness
across a group. Clients are responsible for sending at most one primary product per group.
Luigi’s Box derives two search attributes for every product: lbx:group_id copies
@group_id or falls back to the untyped identity part of the product’s own @id
(product/prod_123 becomes prod_123), and lbx:group_primary copies
@group_primary or falls back to false. The fields you supplied are stored unchanged.
Every group member is stored independently as a complete product. Catalog ingestion does not copy attributes from a primary product onto its variants.
{ "@id": "product/t-shirt-blue-m", "@type": "product", "@title": "Blue Cotton T-Shirt, M", "@group_id": "northwear-t-shirt-1001", "@group_primary": true, "color": "blue", "size": "M"}@group_id must be a non-empty string and @group_primary must be a JSON
boolean, not the strings "true" or "false".
Field namespaces
Section titled “Field namespaces”An object is a flat set of fields. A field’s name says who wrote it and who may read it, through a namespace: a prefix ending in a colon, in front of the field name.
color no prefixprivate:margin the private: namespacelbx:color the lbx: namespaceThere are no per-field visibility settings. The prefix decides, so a field’s name says what it is in a feed, in an API payload and in a discovery response.
| Form | Example | Written by | Who can read it |
|---|---|---|---|
| No prefix | color, price | You | Everyone — including the browser |
@ slot | @id, @title, @category | You (a closed set) | Everyone |
private: | private:margin | You | Server-side integrations only |
lbx: | lbx:color, lbx:group_id | Luigi’s Box | Everyone |
Three rules hold across all of them:
- One prefix, never two. Namespaces do not stack: there is no
private:lbx:something. @is not a namespace. It marks a registered structural slot from a closed set, not a prefix you can extend.- An unrecognized prefix is rejected at ingest. A colon in a field name that is not a
registered namespace fails the object rather than being stored as a literal name — so a
field called
size:euis an error, not an attribute.
What you can write
Section titled “What you can write”You own the fields with no prefix, the registered @ slots, and everything under
private:. Luigi’s Box owns the rest, and sending a field in one of its namespaces is
rejected — including lbx: fields derived from what you sent, like
lbx:group_id.
How to structure your data
Section titled “How to structure your data”Send what you already have, under the names you already use.
Beyond the three required slots — @id, @type, @title — there is no prescribed field
list. Your attributes keep your names, in your language:
{ "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton Dress Shirt", "@category": ["category/dress-shirts"], "@brand": "brand/northwear", "url": "https://example.com/p/blue-cotton-dress-shirt", "image_url": "https://cdn.example.com/sku-1001.jpg", "price": 29.9, "list_price": 39.9, "availability": 1, "color": "blue", "size": "M", "material": "cotton", "sleeve_length": "long", "collar": "spread", "care": ["machine wash 30", "iron medium"], "private:margin": 8.4}color, sleeve_length and collar are not predefined fields. They are attributes this
shop has, and they are searchable because they were sent.
What to include
Section titled “What to include”Include an attribute if you can answer yes to any of these:
| Ask | Example |
|---|---|
| Would a shopper filter or facet on it? | color, size, material, availability |
| Would a shopper sort by it? | price, rating, created_at |
| Does the storefront display it? | title, image_url, url, list_price |
| Does it describe what the thing is? | description, brand, params, specifications |
Ranking works from the text and attributes you provide. Descriptions, specifications and category placement are what let a search for “long sleeve cotton office shirt” find the right product.
Leave out presentation markup, internal keys nobody will query, and anything personal.
What matters more than completeness
Section titled “What matters more than completeness”- Consistent names. The same attribute keeps the same name across every record and every
export.
colorin one product andcolourin another are two attributes. - Consistent types. The first value a field receives fixes its type for the catalog, and
a later conflicting type is rejected rather than coerced.
29.9is a number;"29.90"is text, and a text price can never be range-filtered or shown as a price slider. See Catalog metadata. - Omit rather than fake. A missing value should be absent, not
"",0or"N/A"— those become real values that show up in facets and match filters.
Nesting
Section titled “Nesting”Objects are allowed, and nested fields are addressed by dot-path in filters, facets and field projections:
{ "params": { "material": "cotton", "fit": "regular" }}That field is params.material in filters, facets and field projections. Prefer flat
attributes unless your source data is nested.
Keeping data out of the browser
Section titled “Keeping data out of the browser”Fields with no namespace are public: they are eligible to appear in browser-facing discovery responses, depending on the surface configuration. Because browser tokens and their requests are visible in developer tools, a public field must not contain anything a storefront visitor should not be able to read.
Use the private: namespace for anything that must reach only backend integrations
authenticated with a machine-to-machine token — cost, margin, supplier codes, internal
notes:
{ "color": "blue", "price": 29.9, "private:margin": 8.4}Private fields are never returned to a request authenticated with a browser token. Keep machine-to-machine credentials secret; leaking those credentials can expose private fields.
Which fields a given request actually receives also depends on what it asks for — see Selecting fields.
See also
Section titled “See also”- Object types and relationships — what a catalog can hold
- Selecting fields — which fields a discovery request receives
- Feeds overview
- Product feeds
- Category feeds
- Feed troubleshooting
Was this page helpful?
Thanks.