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.
The Content API is how your system pushes catalog content to Luigi’s Box: you send objects as they are created and changes as they happen, instead of publishing a file for Luigi’s Box to fetch on a schedule.
It is a complete way to run a catalog, not a supplement to one. If your platform can emit what changed, use the Content API: the whole catalog moves through one path, a price change is searchable in seconds rather than on the next fetch, and there is nothing to reconcile.
Feeds are the alternative for platforms that already produce catalog exports and would rather not build a push integration.
Mapping applies here too
Section titled “Mapping applies here too”Content pushed over this API goes through the same field mapping as content arriving from a feed, resolved by object type. So you can send your own field names and shapes here as well — you are not obliged to do the renaming and type conversion yourself before posting.
If your catalog has no mapping for a type, objects are stored as sent.
The three operations
Section titled “The three operations”One path, three verbs:
POST /index/v1/{catalog_id}/index/ Upsert whole objectsPATCH /index/v1/{catalog_id}/index/ Change named fieldsDELETE /index/v1/{catalog_id}/index/ Remove objectsAll three take a batch in {"objects": [...]}, and all three answer 202 Accepted:
{ "status": "accepted" }202 means validated and queued, not searchable. Indexing is asynchronous. A response
carries no per-object result, because at that point the work has not been done.
Every call needs a token for the https://api.<region>.luigisbox.ai/index audience, and a
grant on the catalog — writing and deleting are granted separately, so an integration that
only ever upserts need not be able to delete. See
Server-to-server tokens.
Upsert
Section titled “Upsert”POST sends complete objects. The stored object becomes exactly what you send — fields you
leave out are removed, not preserved. This is the operation to use when your system can
produce the whole record.
curl -X POST 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "objects": [ { "@id": "product/sku-1001", "@type": "product", "@title": "Blue Cotton T-Shirt", "@category": ["category/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.9, "list_price": 39.9, "availability": 1, "color": "blue", "size": "M", "private:margin": 8.4 } ] }'@id and @type are required on every object, and the type prefix in @id must match
@type. Everything else is optional: registered @ fields, your own attributes, and
private: fields.
Partial updates
Section titled “Partial updates”PATCH changes named fields and leaves everything else alone. This is what a price or
stock update should use — it is smaller, it cannot accidentally drop attributes, and it
does not require your system to be able to rebuild the whole record.
curl -X PATCH 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "objects": [ { "@id": "product/sku-1001", "@type": "product", "price": 24.9, "availability": 1 }, { "@id": "product/sku-2087", "@type": "product", "availability": 0 } ] }'The rules:
| You send | Result |
|---|---|
| A field with a value | Set to that value |
| A field omitted | Left unchanged |
"@group_id": null | The product leaves its variant group |
Only @id and @type | Rejected — a partial update must name at least one field |
A partial update with no fields to update is rejected with 422.
Two variant-specific rules follow from “omitted means unchanged”:
- To demote a primary product, send
"@group_primary": falseexplicitly. Omitting the field leaves it primary. @group_primarycan be changed without restating an unchanged@group_id.
See Variant groups.
Delete
Section titled “Delete”DELETE takes identities only:
curl -X DELETE 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "objects": [ { "@id": "product/sku-1001", "@type": "product" }, { "@id": "product/sku-2087", "@type": "product" } ] }'Deleting an object that is not there is not an error. Deleting a product does not touch the categories or brands it referenced — references do not cascade.
Batching
Section titled “Batching”Send objects in batches. One request with 500 price changes costs one round trip and one rate-limit entry; the indexing work is the same as for 500 single-object requests.
def send_batches(client, catalog_id, objects, batch_size=500): """POST objects to the Content API in batches, raising on the first rejection.""" for start in range(0, len(objects), batch_size): response = client.post( f"https://api.eu1.luigisbox.ai/index/v1/{catalog_id}/index/", json={"objects": objects[start : start + batch_size]}, ) response.raise_for_status()Batch validation is all-or-nothing at the request boundary: if one object in the batch is
malformed, the whole request is rejected with 422 and nothing in it is queued. Read
exception_details.validation_errors — the loc path includes the object’s index, so you
can point at the offending record.
Send batches for one catalog in order. Two updates to the same object in flight concurrently can be applied in either order.
What gets rejected
Section titled “What gets rejected”| Cause | Status |
|---|---|
Missing @id or @type | 422 |
@id prefix does not match @type — item/123 sent as a product | 422 |
An unrecognized @ field | 422 |
A field in a namespace Luigi’s Box owns, such as lbx: | 422 |
| An unregistered namespace prefix | 422 |
| A partial update with no fields | 422 |
| A value whose type conflicts with the field’s established type | 422 |
The first value a field ever receives fixes its type for the catalog. If price arrives as
29.9 it is numeric from then on; a later "29.90" as a string is rejected rather than
coerced, because a field that flips between text and number breaks range filters and price
facets. Send consistent types from the start, and check a catalog’s field types with
catalog metadata.
private: is the only namespace you can write to. See
Field namespaces.
Confirming it landed
Section titled “Confirming it landed”Because 202 only means queued, verification is a separate step:
- Search for it. Query the object and see whether the change is reflected.
- Read its timeline. Object history shows every change Luigi’s Box received for one object, and whether each has been applied.
See also
Section titled “See also”- Catalog object model — fields, namespaces, identity
- Object history — did my update land?
- Catalog metadata — what the catalog holds, and field types
- Feeds overview — the other way in, for platforms that already export files
- Backend quickstart
Was this page helpful?
Thanks.