Skip to content

    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.

    Content API

    View source

    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.

    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.

    One path, three verbs:

    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:

    { "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.

    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.

    Terminal window
    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.

    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.

    Terminal window
    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 sendResult
    A field with a valueSet to that value
    A field omittedLeft unchanged
    "@group_id": nullThe product leaves its variant group
    Only @id and @typeRejected — 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.

    DELETE takes identities only:

    Terminal window
    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.

    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.

    CauseStatus
    Missing @id or @type422
    @id prefix does not match @typeitem/123 sent as a product422
    An unrecognized @ field422
    A field in a namespace Luigi’s Box owns, such as lbx:422
    An unregistered namespace prefix422
    A partial update with no fields422
    A value whose type conflicts with the field’s established type422

    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.

    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.