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.
Targeting decides where a campaign can act. It says nothing about which requests fire it (that is the trigger) or which products it affects (that is the rule).
The four dimensions
Section titled “The four dimensions”Each dimension can be listed directly or by tag:
| Dimension | Fields |
|---|---|
| Catalogs | catalog_ids, catalog_tags |
| Channels | channel_ids, channel_tags |
| Surfaces | surface_ids, surface_tags |
| Intents | intents — search, recommender, collection |
Values within one dimension are OR-ed; dimensions are AND-ed. So this targets either catalog, but only on search surfaces:
{ "catalog_ids": ["lbc_8w3k2p", "lbc_4hj9tv"], "intents": ["search"]}An empty dimension means all of it, not none. Leaving intents out targets every
intent; a campaign with no targeting at all covers the whole organization. That default is
deliberate — most campaigns want to be broad — but it also means an accidentally empty
dimension widens a campaign rather than disabling it.
Three rules:
- Narrowing by channel or surface needs a catalog scope. Surface IDs are only unique
within a catalog and a channel belongs to one, so
channel_ids,surface_idsor their tags withoutcatalog_idsorcatalog_tagsis rejected (catalog_scope_required).intentsalone is fine. intentsand surfaces are mutually exclusive. An intent already selects whole families of surfaces, so combining the two is rejected (intents_conflict). To keep a tagged set of surfaces to one intent, tag only that intent’s surfaces, or list them insurface_ids.- Every targeted catalog has to be able to fire. Targeting an intent no catalog in scope
serves is rejected (
catalog_without_targeted_intent), and so is a catalog that hosts none of the targeted surface IDs (catalog_without_targeted_surface): catalogs A and B withsurface_ids: ["lbs_recommender_homepage"]fail when B has no such surface.
The two-catalog example above therefore saves only if both catalogs host a search surface.
Tags make targeting reflow
Section titled “Tags make targeting reflow”A tag is resolved to its members when the campaign is saved and again whenever the tag’s members change. Add a catalog to a tag a campaign targets and every active campaign using it starts serving there, with nothing to re-save; delete the tag and each campaign narrows to the catalogs it names directly. A deleted surface receives no requests, so the campaign stops serving there on its own.
The reflow is not instant. Editing a tag queues the campaigns that use it, and they pick the change up within seconds rather than on the next request.
Each dimension keeps up in its own way. One you left open is stored as “any”, not as a list of what existed at the time, so a campaign that names no surfaces serves a surface added to its catalog tomorrow. One you narrowed by tag follows the tag, as above. One you narrowed by listing ids is exactly that list, and changes only when you edit it.
A campaign for “all German storefronts” therefore keeps meaning that as storefronts come and go. The stored targeting is not the whole answer to “what does this campaign cover”; two endpoints resolve the tags as they are right now.
Tag edits are checked against the campaigns that use the tag. Changing a tag’s members or
deleting it fails with 409, naming the campaign, when the change would leave a live
campaign with targeting the campaign API would refuse: adding a catalog without
lbs_search_main to a tag used by a campaign that targets that surface, for example.
Archived campaigns never block a tag edit.
Deleting a tag also fails with 409 (tag_is_last_narrowing) when it is the only thing
narrowing a live campaign on its dimension. An axis nothing narrows is an axis the campaign does
not restrict, so losing the tag would not stop the campaign — it would spread it to every
catalog in the organization, or every channel or surface of its catalogs. Give the campaign its
own ids on that dimension, or archive it, then delete the tag.
Checking targeting before you save
Section titled “Checking targeting before you save”POST /platform/v1/campaigns/validate-targeting validates a targeting set without
persisting anything, and returns what it resolves to right now:
curl -X POST 'https://api.eu1.luigisbox.ai/platform/v1/campaigns/validate-targeting' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "organization_id": "lbo_r2vn8c", "catalog_tags": ["lbt_k4d7rq"], "intents": ["search"] }'A 200 means the targeting would be accepted, and carries the enumerated scope — every
catalog, channel, surface and intent it can serve. A 422 lists every problem in
exception_details.targeting_problems, each with a stable code, the dimension it concerns and the
offending values. The
Authoring API lists
every code.
It checks targeting only: match and rules are not validated here.
The scope it returns is the same shape a saved campaign’s is, so it carries
serving_problems and serving_checked_at.
Both are always empty here — there is no saved campaign to have checked — so read the
empty list as “not checked”, not “nothing wrong”.
Reading a saved campaign’s scope
Section titled “Reading a saved campaign’s scope”GET /platform/v1/campaigns/{campaign_id}/effective-scope answers the same question for a
campaign that already exists: every catalog, channel, surface and intent its targeting
resolves to right now.
It reads the tags as they stand now, and so does serving: a tag edit queues every active campaign using it, so the two agree once that has drained. If you have just changed a tag and the scope is ahead of what you see served, wait a few seconds and try again.
One case still lags. A campaign that names no catalogs targets every catalog in its organization, and the scope says so the moment a catalog is added — but the campaign does not serve there until it is next saved. Save it, and the two agree.
The scope answers what a campaign covers, not why one request missed it: a campaign can cover the surface you are testing and still not fire, because of its schedule, its trigger or another rule. An empty list in a dimension usually means the campaign targets nothing there — a deleted surface, or a tag that no longer matches anything.
channel_ids is the exception: it lists the channels you created, so a catalog you have not
added one to reports an empty list while the campaign still serves on it. Read an empty
channel_ids as “not narrowed to a channel”, and check the other three dimensions for why a
campaign is not firing.
Catalogs the campaign has not reached
Section titled “Catalogs the campaign has not reached”The scope says which catalogs a campaign targets. serving_problems says which of them it is
not actually serving in yet:
{ "catalog_ids": ["lbc_8w3k2p", "lbc_4hj9tv"], "serving_checked_at": "2026-09-08T09:14:02Z", "serving_problems": [ { "catalog_id": "lbc_4hj9tv", "code": "field_type_conflict", "message": "a trigger field is already recorded with a different type in this catalog" } ]}An active campaign with entries is serving in fewer catalogs than its scope shows. Two codes:
code | Meaning |
|---|---|
field_type_conflict | A trigger field is already recorded under a different type in that catalog. The campaign applies everywhere else. |
unsupported_match | The campaign’s match could not be turned into a query. Usually every targeted catalog is listed. |
Read an empty list together with serving_checked_at. Empty only means no catalog is
known to have failed — with a timestamp that is a clean check, and with null it is a check
that has not run yet. A campaign that is not active reaches nothing and reports neither.
Both need the campaign edited, not retried — saving it again re-attempts every catalog.
A debugging order that works
Section titled “A debugging order that works”When a campaign is not doing what you expect, check in this order — each step rules out everything below it:
- Status and schedule. Is it
active, and is now inside its window? Onlyactivecampaigns are served at all. - Effective scope. Does it cover the catalog, channel and surface you are testing —
and is the catalog absent from
serving_problems? - Trigger. Does
matchaccept the request you are making? - Rule selector. Does
record_matchselect the products you expect? - Conflicts. Is another rule beating this one? See Resolving conflicts.
See also
Section titled “See also”- Triggers — which requests a campaign fires on
- Account structure — catalogs, channels, surfaces and tags
- Authoring API — the endpoints in full
Was this page helpful?
Thanks.