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.

    Targeting

    View source

    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).

    Each dimension can be listed directly or by tag:

    DimensionFields
    Catalogscatalog_ids, catalog_tags
    Channelschannel_ids, channel_tags
    Surfacessurface_ids, surface_tags
    Intentsintents — 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_ids or their tags without catalog_ids or catalog_tags is rejected (catalog_scope_required). intents alone is fine.
    • intents and 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 in surface_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 with surface_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.

    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.

    POST /platform/v1/campaigns/validate-targeting validates a targeting set without persisting anything, and returns what it resolves to right now:

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

    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.

    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:

    codeMeaning
    field_type_conflictA trigger field is already recorded under a different type in that catalog. The campaign applies everywhere else.
    unsupported_matchThe 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.

    When a campaign is not doing what you expect, check in this order — each step rules out everything below it:

    1. Status and schedule. Is it active, and is now inside its window? Only active campaigns are served at all.
    2. Effective scope. Does it cover the catalog, channel and surface you are testing — and is the catalog absent from serving_problems?
    3. Trigger. Does match accept the request you are making?
    4. Rule selector. Does record_match select the products you expect?
    5. Conflicts. Is another rule beating this one? See Resolving conflicts.