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.
const url = 'https://api.eu1.luigisbox.ai/platform/v1/campaigns/validate-targeting';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"catalog_ids":["example"],"catalog_tags":["example"],"channel_ids":["example"],"channel_tags":["example"],"intents":["search"],"organization_id":"example","surface_ids":["example"],"surface_tags":["example"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.eu1.luigisbox.ai/platform/v1/campaigns/validate-targeting \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "catalog_ids": [ "example" ], "catalog_tags": [ "example" ], "channel_ids": [ "example" ], "channel_tags": [ "example" ], "intents": [ "search" ], "organization_id": "example", "surface_ids": [ "example" ], "surface_tags": [ "example" ] }'Validate a campaign’s targeting without persisting anything. 200 means the targeting would be accepted and returns the scope it resolves to right now — every catalog, channel, surface and Intent it can serve, enumerated; 422 lists every problem in exception_details.targeting_problems, each with a stable code, its dimension and the offending values. match and rules are not checked.
The scope carries serving_problems and serving_checked_at because it is the same shape a saved campaign’s does, and both are always empty here: a dry run has no saved campaign to have checked, so the empty list reads as “not checked yet” rather than “nothing wrong”.
Authorized against the organization_id in the body: credentials granted on that organization are enough, while a grant on one of its catalogs is not.
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”A campaign’s targeting on its own — no name, no match, no rules, no persistence.
Ids are canonicalized, deduped and format-checked exactly as they are on a real
write, and every targeting violation is reported uniformly as a problems entry.
object
Catalogs this campaign narrows to, by id. catalog_tags narrows the same axis by contributing its members, so the scope is the union of the two. An axis nothing narrows is an axis the campaign does not restrict, so empty means every one of them rather than none. A catalog outside the organization is rejected with catalog_not_in_organization.
Catalog tags whose members join catalog_ids on the catalog axis. Resolved at read time, so editing a tag reflows into every campaign referencing it. A tag that does not exist in the organization is rejected with tag_not_found, and one carrying another dimension with tag_dimension_mismatch.
Channels this campaign narrows to, by id, jointly with channel_tags. An axis nothing narrows is an axis the campaign does not restrict, so empty means every one of them rather than none. Narrowing by channel requires a catalog scope (catalog_scope_required), the channel must be in the organization (channel_not_in_organization) and owned by a scoped catalog (channel_outside_catalog_scope).
Channel tags whose members join channel_ids on the channel axis, under the same resolution and the same tag_not_found / tag_dimension_mismatch rejections.
Surface Intents this campaign targets — the kind of discovery it applies to. Empty means every Intent, not none. Narrowing by Intent is mutually exclusive with narrowing by surface_ids / surface_tags: an Intent targets whole families of surfaces, so combining the two is rejected with intents_conflict. A targeted Intent no scoped catalog hosts a surface for is rejected with catalog_without_targeted_intent — it could never trigger.
Surfaces this campaign narrows to, by id, jointly with surface_tags. An axis nothing narrows is an axis the campaign does not restrict, so empty means every one of them rather than none. Mutually exclusive with intents (intents_conflict), requires a catalog scope (catalog_scope_required), and each surface must sit under a scoped catalog (surface_not_in_catalog_scope).
Surface tags whose members join surface_ids on the surface axis, under the same resolution and rejections. A member whose own catalog is outside the catalog scope is tolerated rather than rejected, which keeps a surface tag reusable across catalogs.
Responses
Section titled “ Responses ”Successful Response
The scope an accepted targeting set resolves to; validity is carried by the 200.
object
What a campaign’s targeting resolves to right now, fully enumerated.
Explicit picks are unioned with the picked tags’ current members and resolved against the catalogs, channels and surfaces that exist at read time. Serving resolves the same way, so this is also what the campaign is covering: a tag edit reaches serving within seconds, and only inside that window can the two differ.
One case lags longer. A campaign that names no catalogs covers every catalog in its organization, so a newly added one is listed here at once — but the campaign does not serve there until it is next saved.
A dimension with no reference at all widens to its universe — all catalogs of the
organization, all channels and surfaces of those catalogs. An empty list therefore means
the campaign targets nothing in that dimension, with one exception spelled out for
channel_ids below.
intents is derived from surfaces rather than echoing the stored array: a
campaign whose targeted surface was deleted reports no surfaces and no Intents.
A channel referenced explicitly, or through a channel tag, is always listed in
channel_ids. A campaign that narrows by no channel is unrestricted by channel at
request time, whatever channel_ids reports.
object
The catalogs the campaign resolves to. Empty means it resolves to none, so nothing serves it — not that it reaches all of them.
The channels the campaign narrows to, excluding each catalog’s implicit default channel. Empty means it narrows to no channel you created — not that it reaches none, since a campaign unnarrowed by channel is dispatched to every channel of its catalogs.
The distinct Intents of surfaces, sorted — derived at read time, never the stored targeting echoed back. A campaign whose targeted surface has since been deleted reports no surfaces and no Intents.
Targeted catalogs the campaign is not currently serving in, one entry each. A non-empty list means the campaign reads as active while reaching fewer catalogs than its scope shows. Empty means no catalog is known to have failed, which is not the same as confirmed healthy — read it with serving_checked_at: empty with a timestamp is a clean check, empty with null is one that has not run yet. A campaign that is not active reaches nothing and reports none, and a dry run of unsaved targeting has no campaign to check, so both are always empty.
One targeted catalog an active campaign is not currently serving in.
object
The targeted catalog the campaign is not serving in.
Stable machine code for why the campaign does not reach this catalog.
Human-readable explanation of the failure.
The surfaces the campaign resolves to, each with the catalog that owns it. Empty means it narrows to no surface — not that it reaches none, since a campaign unnarrowed by surface is served on every surface of its catalogs whose Intent it targets.
A concrete surface reference: a surface id paired with its catalog.
object
Example
{ "effective_scope": { "intents": [ "search" ], "serving_problems": [ { "code": "field_type_conflict" } ] }}Missing or invalid credentials
Canonical error body — every non-2xx response uses this shape.
object
Machine-readable error context; empty object when there is nothing to add.
object
Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. filter_unknown_field or cursor_mismatch. Safe to branch on; reason is not. Absent when the status code says all there is to say.
Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value.
One field-level reason a request was rejected.
object
Path to the offending value from the request root, e.g. ["body", "filters", 0, "operator"].
What is wrong with the value at loc.
Stable machine code for the problem, e.g. missing or string_too_short.
Human-readable, actionable error message.
Example generated
{ "exception_details": { "error_code": "example", "validation_errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ] }, "reason": "example", "request_id": "example"}Authenticated but not permitted
Canonical error body — every non-2xx response uses this shape.
object
Machine-readable error context; empty object when there is nothing to add.
object
Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. filter_unknown_field or cursor_mismatch. Safe to branch on; reason is not. Absent when the status code says all there is to say.
Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value.
One field-level reason a request was rejected.
object
Path to the offending value from the request root, e.g. ["body", "filters", 0, "operator"].
What is wrong with the value at loc.
Stable machine code for the problem, e.g. missing or string_too_short.
Human-readable, actionable error message.
Example generated
{ "exception_details": { "error_code": "example", "validation_errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ] }, "reason": "example", "request_id": "example"}Request validation failed. A targeting rejection lists every problem at once in exception_details.targeting_problems.
The canonical error body of a rejected targeting set, with problems typed.
reason and request_id are the canonical fields; exception_details carries
the typed problems list.
object
The exception_details of a rejected targeting set: every problem found at once.
object
Every targeting violation found in one pass; never truncated. Named for what it carries, so it sits beside filter_problems on the same 422 without a client having to work out which of the two it was given. A 422 carries one list or the other, never both: the first check to refuse ends the request.
One targeting violation: a stable machine code plus the offending values.
object
Stable machine code of the violated rule.
The targeting axis the violation is about.
Human-readable explanation of the violation.
The offending values on that axis.
Human-readable, actionable error message.
Canonical error body — every non-2xx response uses this shape.
object
Machine-readable error context; empty object when there is nothing to add.
object
Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. filter_unknown_field or cursor_mismatch. Safe to branch on; reason is not. Absent when the status code says all there is to say.
Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value.
One field-level reason a request was rejected.
object
Path to the offending value from the request root, e.g. ["body", "filters", 0, "operator"].
What is wrong with the value at loc.
Stable machine code for the problem, e.g. missing or string_too_short.
Human-readable, actionable error message.
Example
{ "exception_details": { "targeting_problems": [ { "code": "catalog_scope_required", "dimension": "catalog" } ] }}Was this page helpful?
Thanks.