--- title: Content API description: Push catalog objects to Luigi's Box AI as they change — upsert, partial update and delete, with the semantics of each. slug: indexing/content-api docKind: guide hub: luigisbox-ai --- 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](/indexing/feeds/) are the alternative for platforms that already produce catalog exports and would rather not build a push integration. :::tip[Pick one path per object type] Do not split one object type across both — a feed for the snapshot, API calls for a few fields. A feed run sends the whole object and overwrites whatever the API most recently set. If you push over the API, let it own the content. ::: ## Mapping applies here too Content pushed over this API goes through the same [field mapping](/indexing/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 One path, three verbs: ```text POST /index/v1/{catalog_id}/index/ Upsert whole objects PATCH /index/v1/{catalog_id}/index/ Change named fields DELETE /index/v1/{catalog_id}/index/ Remove objects ``` All three take a batch in `{"objects": [...]}`, and all three answer `202 Accepted`: ```json { "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. :::note[The trailing slash is part of the path] `/index/v1/lbc_8w3k2p/index/` — with the final slash. Without it the request does not route. ::: Every call needs a token for the `https://api..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](/authentication/server-to-server/). ## 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. ```bash curl -X POST 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer ' \ -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 `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. ```bash curl -X PATCH 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer ' \ -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": false` explicitly. Omitting the field leaves it primary. - `@group_primary` can be changed without restating an unchanged `@group_id`. See [Variant groups](/concepts/catalog-object-model/#variant-groups). ## Delete `DELETE` takes identities only: ```bash curl -X DELETE 'https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/' \ -H 'Authorization: Bearer ' \ -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. :::caution[Deletes are not reversible] There is no undelete. Re-sending the object recreates it, but its learned ranking signals start over. Prefer marking a product unavailable (`availability = 0`, plus a channel visibility rule) over deleting it, if it might come back. ::: ## 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. ```python 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 | 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](/indexing/catalog-metadata/). `private:` is the only namespace you can write to. See [Field namespaces](/concepts/catalog-object-model/#field-namespaces). ## 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](/indexing/object-history/) shows every change Luigi's Box received for one object, and whether each has been applied. ## See also - [Catalog object model](/concepts/catalog-object-model/) — fields, namespaces, identity - [Object history](/indexing/object-history/) — did my update land? - [Catalog metadata](/indexing/catalog-metadata/) — what the catalog holds, and field types - [Feeds overview](/indexing/feeds/) — the other way in, for platforms that already export files - [Backend quickstart](/start-here/backend-quickstart/)