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.

    Requests and responses

    View source

    The Luigi’s Box AI API is one host, and the conventions on this page hold across every endpoint on it.

    https://api.<region>.luigisbox.ai/<service>/v1/<resource>
    • <region> is the zone your account runs in, for example eu1. Examples here use eu1.
    • <service> is the product prefix: discovery, index, catalog, ledger, events, platform. One host, one path prefix per product.
    • v1 is the URL namespace, not a stability promise. It only changes for a breaking revision, and a new version runs alongside the old one. While the API is pre-launch, the shape inside v1 can still change — see the notice at the top of every page.
    https://api.eu1.luigisbox.ai/discovery/v1/search
    https://api.eu1.luigisbox.ai/index/v1/lbc_8w3k2p/index/
    https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/sources/feeds
    https://api.eu1.luigisbox.ai/platform/v1/channels

    Tokens are minted on a separate host, https://auth.<region>.luigisbox.ai. See Authentication.

    Every timestamp field ends in _at and is UTC ISO-8601:

    { "created_at": "2026-03-14T09:21:07Z" }
    HeaderWhenPurpose
    Authorization: Bearer <token>AlwaysYour access token
    Content-Type: application/jsonOn requests with a body
    X-Lbx-Visitor-IdRequired on discovery and event requestsIdentifies the browser
    X-Request-IdOptionalYour own correlation ID; echoed back
    HeaderWhenPurpose
    X-Request-IdAlwaysYours if you sent one, otherwise generated. Quote it in support requests.
    RateLimit-Limit, RateLimit-Remaining, RateLimit-ResetRate-limited endpointsYour current budget
    Retry-AfterOn 429How long to wait

    Discovery and analytics need to know which shopper a request belongs to, without knowing who they are. X-Lbx-Visitor-Id carries that.

    ScopeOne browser, long-lived
    Who generates itYour integration
    RequiredYes, on discovery requests and on analytics events
    Survives a token refreshYes

    It is an opaque string you own; a UUID is the usual choice. It is not used to authorize anything, so it does not need to be secret. It does need to be consistent, because it is what joins a shopper’s discovery requests to the analytics events that follow.

    If your backend generates this value instead of passing through what your frontend produced, every shopper collapses into one: personalization stops working and reports stop being meaningful.

    A signed-in shopper additionally passes user_id — a body field on discovery requests, a field on events. It is separate and optional; see Personalization.

    Endpoints that list stored records page with page and size, and return a consistent envelope:

    Terminal window
    curl -G 'https://api.eu1.luigisbox.ai/platform/v1/channels' \
    --data-urlencode 'catalog_id=lbc_8w3k2p' \
    --data-urlencode 'page=2' \
    --data-urlencode 'size=50' \
    -H 'Authorization: Bearer <token>'

    The envelope is the same on every such endpoint:

    {
    "items": [{ "id": "lbn_4hj9tv", "name": "example.com" }],
    "page": 2,
    "size": 50,
    "total": 61
    }

    Discovery results are different: they page with an opaque cursor, because a ranked result set is not a stable list of rows. See Sorting and pagination.

    On collection endpoints, one parameter, field:direction, with the direction always spelled out:

    ?sort=created_at:desc
    ?sort=price:asc

    There is no multi-field sort and no -field shorthand. A field is sortable only if the response shows it.

    Discovery sorts differently: its requests are JSON, so sort is an object rather than a string. See Sorting and pagination.

    Every collection has a documented, deterministic default order on a value that does not change — usually id or created_at — so paging through it twice gives you the same records in the same order.

    A PATCH body means “change exactly what I named”:

    • A field you omit is left unchanged.
    • A field you send as null is cleared, when the field can be cleared.
    • A field that cannot be cleared rejects null with 422.

    This changes a feed’s URL and nothing else:

    Terminal window
    curl -X PATCH 'https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/sources/feeds/41' \
    -H 'Authorization: Bearer <token>' \
    -H 'Content-Type: application/json' \
    -d '{"url": "https://example.com/feeds/products.xml.gz", "expected_version": 7}'

    The same holds for partial updates to catalog objects, where omitting a field is how you leave a stored attribute alone.

    A successful POST, PUT or PATCH responds with the complete resource — server-assigned IDs, resolved defaults and computed fields included. No follow-up GET is needed to read back what was written.

    Secrets are the exception: they are accepted and never echoed back. A resource that holds one reports whether it is set rather than what it is.

    Deletes respond 204 with no body. Requests that are accepted for asynchronous processing respond 202 — see Content API.

    Every endpoint on the surface is described by one OpenAPI 3.1 document, and the API Reference is generated from it. The same document is downloadable:

    Terminal window
    curl -O https://docs.luigisbox.ai/openapi.yaml

    It is the authoritative description of the API — paths, parameters, request and response schemas, enums, defaults, and the error shape. Where this documentation and the contract disagree, the contract is right.

    Point a generator at the spec to get a typed client in your language:

    Terminal window
    # TypeScript types
    npx openapi-typescript https://docs.luigisbox.ai/openapi.yaml -o src/luigisbox.d.ts
    # A full client, any of ~50 languages
    npx @openapitools/openapi-generator-cli generate \
    -i https://docs.luigisbox.ai/openapi.yaml \
    -g python \
    -o ./luigisbox-client

    Any OpenAPI 3.1 generator works — openapi-generator, openapi-typescript, oapi-codegen, kiota, NSwag. Luigi’s Box does not publish SDKs, so a generated client is the supported way to get one, and it is regenerable rather than something you maintain by hand.

    Two properties of the contract affect the generated code:

    • Operation IDs are curated, verb first — list_catalogs, index_objects, get_object_timeline — so generated method names read like the API rather than like the URL.
    • One schema name means one thing surface-wide. A Catalog is the same Catalog whichever service returns it, so a generated model is shared rather than duplicated per endpoint.

    Regenerate when you pull a new spec. While the API is pre-launch that regeneration can change request and response shapes, not only add to them, so review the diff rather than assuming your existing calls still compile.

    Once the API launches the contract only grows within v1 — new endpoints, new optional fields, new enum values — and anything that would break a caller lands in a new version path running alongside the old one.

    If your generator is strict about unknown enum values, allow for them: a value added to an enum is not a breaking change on our side, and a client that hard-fails on one will break on a change we consider additive.

    • Mock server. npx @stoplight/prism-cli mock openapi.yaml gives you a local API that answers with schema-valid responses — useful before your credentials arrive.
    • Request validation in tests. Assert your payloads against the schemas before sending them.
    • Import into a client. Postman, Insomnia, Bruno and Paw all read the spec directly.