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.
Every call you make names at least one of these entities. Most accounts are set up once with Luigi’s Box and rarely change shape afterwards, but reading them — and adding a market or a placement — is something you can do yourself.
The domain model explains what these entities are. This page is about working with them over the API.
Organization lbo_└── Catalog lbc_ ├── Channel lbn_ ├── Surface lbs_<intent>_<slug> └── Collection collection:…Tag lbt_ (groups catalogs, channels or surfaces)Everything here is under /platform/v1/, needs a token for the
https://api.<region>.luigisbox.ai/platform audience, and a grant to read or manage kernel
entities — reading, writing and deleting are granted separately.
What can I see?
Section titled “What can I see?”One call returns the whole tree your credentials can reach. It is paginated by organization and defaults to 20, so ask for a page large enough to hold it:
curl -G 'https://api.eu1.luigisbox.ai/platform/v1/me/entitlements' \ -d 'size=1000' \ -H 'Authorization: Bearer <token>'Everything your credentials can reach, nested:
{ "items": [ { "id": "lbo_r2vn8c", "name": "Example Retail", "catalogs": [ { "id": "lbc_8w3k2p", "name": "Example — EN", "language": "en_GB", "channels": [{ "id": "lbn_4hj9tv", "name": "example.com" }], "surfaces": [ { "id": "lbs_search_main", "intent": "search", "slug": "main" } ] } ] } ], "page": 1, "size": 1000, "total": 1}The response is scoped to your own credentials — it lists exactly what you are entitled to and nothing else — so it answers which catalog to index into and which channel to search on.
Paginated by organization with page and size, in the same envelope every list on the
API uses: one items entry is one organization, with its whole subtree nested inside, and
total counts organizations. size defaults to 20 and accepts up to 1000 here —
higher than the usual limit of 100, so a token entitled to many organizations can read the
whole tree in one call.
Organizations
Section titled “Organizations”Your company. The billing entity and the root of everything else.
GET /platform/v1/organizationsPOST /platform/v1/organizationsGET /platform/v1/organizations/{organization_id}PATCH /platform/v1/organizations/{organization_id}DELETE /platform/v1/organizations/{organization_id}An organization has a name and nothing else to configure. Most integrations only ever
read this.
Catalogs
Section titled “Catalogs”A catalog is one set of objects in one language, with its own settings. Separate catalogs are how you handle separate languages or regions.
Catalogs are created by Luigi’s Box: POST /platform/v1/catalogs needs credentials that span
every organization, so ask your Luigi’s Box contact for a new one. The request below is what
that call takes.
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/catalogs' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "lbo_r2vn8c", "name": "Example — German", "language": "de_DE" }'| Field | Required | Notes |
|---|---|---|
organization_id | Yes | |
name | Yes | |
language | Yes | xx_XX — a lowercase language, an underscore, an uppercase region |
settings | No | Catalog-level settings |
seed_surface_templates | No | Templates to seed the catalog’s first surfaces from |
The language decides how text is analyzed: de_DE gets German stemming and compound
handling, so a search for “Schuhe” finds Wanderschuhe.
A new catalog comes with two surfaces, each already serving a first version:
lbs_search_default and lbs_collection_default. The collection surface answers with products
only; catalogs created before it was added don’t have it, so ask your Luigi’s Box contact to add
one. A new catalog has no recommender surface: each recommender gets its own when it is set up.
Older catalogs may also carry lbs_recommender_default, which you can ignore. Templates named in
seed_surface_templates add further surfaces on top of these two; recommender templates are not
meant for it, as each recommender is set up with its own settings.
List with ?organization_id=… — that parameter is also the authorization scope, see
Status codes. Reading, updating and
deleting follow the usual shape.
Channels
Section titled “Channels”A channel is a serving destination and a view over one catalog. Two mechanisms make one catalog serve several markets without duplicating content:
| Mechanism | Field | What it does |
|---|---|---|
| Visibility filter | visibility_filter | Which objects this channel may serve at all |
| Attribute aliases | attribute_aliases | Which of an object’s destination-specific values this channel serves, and under which name |
Catalogs and channels covers both in full, including which of an aliased field’s two names a response is keyed by. What follows is the API surface.
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/channels' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "catalog_id": "lbc_8w3k2p", "name": "example.de", "attribute_aliases": { "price": "price_de", "availability": "availability_de" } }'Each product carries several destination-specific values — price_de, price_at — and the
channel decides which one answers to the canonical name price. Your storefront asks for
price and gets the right one, with no per-request context and no duplicated products.
Both fields are validated against the catalog when you write them, so a name the catalog
does not carry — or an operator its type does not allow — is a 422. The check needs a catalog with content, so neither
field can be set before the catalog’s first ingest: create the channel without them and set
them once it has been fed. When the check cannot run at all, the write fails with a
retryable 503 instead of saving unvalidated. See
Attribute aliases
for the full list of what is rejected and why.
visibility_filter is the one clearable field on a channel: send null to remove it.
Everything else, omit to leave unchanged. See
Partial updates.
List with ?catalog_id=… for one catalog’s channels, or ?organization_id=… for every
channel in an organization. Sending both narrows to the intersection.
Surfaces
Section titled “Surfaces”A surface is a configured discovery unit, scoped to a catalog. Every channel bound to that catalog can call any of its surfaces — there is no per-channel duplication.
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/catalogs/lbc_8w3k2p/surfaces' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "intent": "search", "slug": "main" }'| Field | Required | Notes |
|---|---|---|
intent | Yes | The intent: search, recommender or collection |
slug | Yes | Lowercase letters, digits and underscores, starting with a letter, 1–48 characters |
The two combine into the surface’s ID — lbs_search_main — and slugs are unique per
catalog and intent. See
Surface identifiers.
A surface’s response reports its default_version. Ranking configuration lives on versions
rather than on the surface itself, which is what lets an A/B test run several versions of one
surface against each other without duplicating it. Version authoring is done with Luigi’s Box; the surface
endpoints here create, read, list and delete the surface itself.
GET /platform/v1/catalogs/{catalog_id}/surfacesPOST /platform/v1/catalogs/{catalog_id}/surfacesGET /platform/v1/catalogs/{catalog_id}/surfaces/{surface_id}DELETE /platform/v1/catalogs/{catalog_id}/surfaces/{surface_id}Collections
Section titled “Collections”A collection is what a collection request serves. Two kinds are
registered on the catalog before a request can name them; the third,
collection:all/member:{member_type}, lists every object of a type and needs no registration.
A membership collection turns every object of one type into a listing of another type. One registration covers every category, for example:
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/catalogs/lbc_8w3k2p/collections' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "kind": "membership", "collection_type": "category", "member_type": "product", "membership_attribute": "lbx:category_ids", "ancestry_attribute": "lbx:category_ids" }'| Field | Required | Notes |
|---|---|---|
collection_type | Yes | Object type whose objects are the collections, e.g. category |
member_type | Yes | Object type the collection returns, e.g. product |
membership_attribute | Yes | Attribute of the member type holding the @id of every collection object the member belongs to, e.g. lbx:category_ids |
ancestry_attribute | No | Attribute of the collection type holding each object’s ancestor @ids, e.g. lbx:category_ids; omit it for a type without hierarchy |
lbx:category_ids is derived from your categories’ @parent. On a product it holds every
category the product is in and every category above them, so
collection:category/clothing/member:product lists the products of its subcategories too. On a
category it holds the category’s ancestors, then the category itself. Without @parent, a
category’s collection lists only the products that reference it directly.
Its ID is collection:category/member:product, and one registration exists per
collection_type and member_type pair. A request serves one category of it as
collection:category/shirts/member:product.
A curated collection is one named filter over a member type.
{ "kind": "curated", "slug": "new_arrivals", "name": "New Arrivals", "member_type": "product", "filters": { "label": { "eq": "new" } }}| Field | Required | Notes |
|---|---|---|
slug | Yes | Your part of the ID: lowercase ASCII letters, digits, _ and -, up to 256 characters |
name | Yes | Shown where the collection is listed; it takes no part in the ID |
member_type | Yes | Object type the collection returns |
filters | Yes | A filter tree in map syntax over the member type; negations are stored with an exists, see missing attributes |
Its ID is collection:curated/new_arrivals/member:product, so you know it before the
request is answered. slug is taken as sent: a value outside the format above is a
422, never adjusted. One slug registers one collection per member type;
registering a second one is a 409. A request names a curated collection by this ID and
lists the objects matching filters.
Only filters can change after registration:
curl -X PATCH 'https://api.eu1.luigisbox.ai/platform/v1/catalogs/lbc_8w3k2p/collections/collection:curated/new_arrivals/member:product' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "filters": { "label": { "eq": "new" }, "stock": { "gt": 0 } } }'The response is the collection with the new filters and a later updated_at. Updating a
membership collection is a 422.
A field of the other kind is a 422. Types and attributes are checked against what the
catalog has ingested, so register collections after the catalog’s first feed run:
- an unknown type or attribute is a
422naming it, - an attribute starting with
@skips that check, though a filter value on one is still checked, - an attribute in the
private:,signals:,ml:orlexical:namespace is a422, - a catalog nothing has been ingested into yet is a
422.
A catalog holds at most 1,000 collections. Apart from a curated collection’s filters, a
collection cannot be changed; delete it and register it again. Its ID contains / and is
sent in the path as is.
GET /platform/v1/catalogs/{catalog_id}/collectionsPOST /platform/v1/catalogs/{catalog_id}/collectionsGET /platform/v1/catalogs/{catalog_id}/collections/{collection_id}PATCH /platform/v1/catalogs/{catalog_id}/collections/{collection_id}DELETE /platform/v1/catalogs/{catalog_id}/collections/{collection_id}A tag is a named group, and it exists so that things which target many entities — chiefly campaigns — can target a group instead of a list.
One tag groups exactly one dimension:
dimension | Members are |
|---|---|
catalog | Catalog IDs |
channel | Channel IDs |
surface | {catalog_id, surface_id} pairs |
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/tags' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "lbo_r2vn8c", "dimension": "catalog", "name": "market:eu", "members": ["lbc_8w3k2p", "lbc_4hj9tv"] }'Surface tags name the catalog alongside the surface, because a surface ID is only unique within its catalog:
{ "organization_id": "lbo_r2vn8c", "dimension": "surface", "name": "surface-group:all-search", "members": [ { "catalog_id": "lbc_8w3k2p", "surface_id": "lbs_search_main" }, { "catalog_id": "lbc_4hj9tv", "surface_id": "lbs_search_main" } ]}At least one member is required, and dimension is immutable — changing it would orphan the
members.
A PATCH edits membership as a delta rather than replacing the set: add_members and
remove_members each name only what changes, so two people editing different members of the
same tag cannot erase each other’s work. Both are applied idempotently, and the two lists must
not overlap.
Naming a tag
Section titled “Naming a tag”A tag name is namespace:value — a lowercase namespace, a colon, then the value:
market:euseason:black-fridaysurface-group:all-searchThe namespace is yours to choose, and its job is to say what kind of grouping the tag is, so
that a list of them reads as an index rather than a pile. Pick one per axis you group along —
market:, season:, campaign-type: — and reuse it; a namespace nobody reuses is a prefix,
not a namespace. Names are lowercased on the way in, so Market:EU and market:eu are the
same tag.
The namespace must start with a letter and may hold letters, digits, - and _. The value
after the colon may not contain whitespace or a further colon.
Some namespaces are reserved for tags Luigi’s Box manages itself — currently intent,
language and recommender. Writing or deleting a tag in one of those is refused with a
403 naming the namespace, unless your token carries the matching reserved-tags permission,
granted on the tag’s organization or wider.
Choose your own namespace and the question never arises.
A tag cannot be deleted while it is the only thing narrowing a live campaign on its dimension:
that campaign names nothing of its own there, so losing the tag would spread it across the whole
dimension rather than stop it. The delete answers 409 and names the campaign. Give the campaign
its own ids on that dimension, or archive it, then delete the tag.
Tags reflow
Section titled “Tags reflow”A campaign targeting market:eu resolves that tag to its members, and re-resolves it whenever the tag changes. Add a catalog to the tag and every active campaign targeting it covers the new market within seconds, with no campaign edited.
Editing a tag therefore changes what every campaign using it serves. Before changing
membership, check which campaigns point at it —
GET /platform/v1/campaigns?intent=… and
each campaign’s
effective scope answer that.
List with ?organization_id=…, optionally &dimension=….
Setting up a new market
Section titled “Setting up a new market”The usual sequence, when an organization already exists:
- Ask Luigi’s Box to create the catalog for the language — see Catalogs.
- Register its feeds and mappings — Feed management API.
- Create the channel for the storefront, with its visibility filter and attribute projection.
- Create any further surfaces the storefront will call (the catalog already has three), and register the collections its listing pages serve.
- Ask Luigi’s Box for credentials — a publishable key bound to the new channel, or an OAuth client for your backend.
- Verify with
/me/entitlementsand one search request.
Steps 2 and 4 determine most of the ranking configuration. Do both with your Luigi’s Box contact.
See also
Section titled “See also”- Domain model — what these entities mean
- Identifiers — their formats
- Business rules — the main consumer of tags
- Feed management API — filling a new catalog
- Authentication overview — credentials for a new channel
Was this page helpful?
Thanks.