--- title: Authoring API description: The endpoints that create, read, update, list and archive campaigns — request shapes, update semantics, filters and limits. slug: merchandising/business-rules/authoring-api docKind: reference hub: luigisbox-ai --- Campaigns live on the platform API and need a token for the `https://api..luigisbox.ai/platform` audience. ```text POST /platform/v1/campaigns GET /platform/v1/campaigns GET /platform/v1/campaigns/{campaign_id} PATCH /platform/v1/campaigns/{campaign_id} DELETE /platform/v1/campaigns/{campaign_id} POST /platform/v1/campaigns/validate-targeting POST /platform/v1/campaigns/trigger-schema GET /platform/v1/campaigns/{campaign_id}/effective-scope ``` The first five are the resource; the last three are authoring aids — they resolve targeting and tell you what is authorable, without persisting anything. They are covered under [Targeting](/merchandising/business-rules/targeting/#tags-make-targeting-reflow) and [Triggers](/merchandising/business-rules/triggers/#discovering-what-you-can-trigger-on). :::hub[Config → Business Rules]{path="/business-rules"} Everything on this page except deletion has a form in the Hub: creating a campaign, editing it, listing and filtering campaigns by effect and surface type, and moving one between draft, active, paused and archived. The two authoring aids run behind that editor, so the targeting it accepts and the trigger fields it offers are the ones these endpoints resolve. ::: ## Creating a campaign | Field | Required | Notes | |---|---|---| | `organization_id` | Yes | Immutable | | `name` | Yes | 1–256 characters | | `match` | Yes | The trigger tree | | `rules` | Yes | 1–50 rules | | `status` | No | `draft`, `active`, `paused`. Defaults to `draft`. | | `starts_at` / `ends_at` | No | UTC instants | | `timezone` | No | Informational; records the zone the schedule was authored in | | `catalog_ids`, `channel_ids`, `surface_ids`, `intents` | No | Targeting; empty means all | | `catalog_tags`, `channel_tags`, `surface_tags` | No | Targeting by tag | Each rule: | Field | Required | Notes | |---|---|---| | `name` | Yes | 1–256 characters | | `record_match` | Yes | Product selector. Must select something for a `pin`. | | `effect` | Yes | One of `pin`, `ban`, `boost`, `bury` | The response is the created campaign with its `lbm_…` ID, each rule's `lbr_…` ID, and audit fields. See [Overview](/merchandising/business-rules/overview/#a-first-campaign) for a complete request. ### Effect shapes ```json { "type": "pin", "position": 1 } { "type": "ban" } { "type": "boost", "strength_pct": 0.4, "mode": "hard" } { "type": "bury", "strength_pct": 0.6, "mode": "soft", "decay_window": "P7D" } ``` `position` is a positive integer; `strength_pct` is between 0 and 1 inclusive; `decay_window` is an ISO 8601 duration, valid only with `mode: "soft"`, and must be positive. `mode` applies to `boost` and `bury` and defaults to `hard`. ### Validation you will meet | Situation | Result | |---|---| | A `name` already used by another live campaign in the organization | `409` | | `channel_ids`, `surface_ids` or their tags without `catalog_ids` / `catalog_tags` | `422`, code `catalog_scope_required` | | `intents` together with `surface_ids` / `surface_tags` | `422`, code `intents_conflict` | | A catalog or channel from another organization, directly or through a tag | `422`, code `catalog_not_in_organization` / `channel_not_in_organization` | | A tag that does not exist in the organization | `422`, code `tag_not_found` | | A tag of one dimension used in another, such as a channel tag in `catalog_tags` | `422`, code `tag_dimension_mismatch` | | A surface ID that exists in none of the targeted catalogs | `422`, code `surface_not_in_catalog_scope` | | A targeted catalog hosting none of the targeted surface IDs | `422`, code `catalog_without_targeted_surface` | | A channel whose catalog is not among the targeted catalogs | `422`, code `channel_outside_catalog_scope` | | An intent no targeted catalog serves | `422`, code `catalog_without_targeted_intent` | | A `pin` whose `record_match` has no conditions | `422` | | `decay_window` on `mode: "hard"`, or ≤ 0 | `422` | | A condition tree deeper than 10 levels or over 100 conditions | `422` | | An operator the field does not accept in some targeted catalog | `422` | | A `trigger.request:*` field that is not one of the four | `422` | | `starts_at` not strictly before `ends_at` | `422` | | `timezone` with neither `starts_at` nor `ends_at` | `422` | Targeting problems come back in `exception_details.problems`, each with a stable `code`, its `dimension` and the offending `values` — so an authoring UI can point at the field that is wrong rather than showing one message. ## Updating `PATCH` is partial: a field the body omits is left unchanged. ```bash curl -X PATCH 'https://api.eu1.luigisbox.ai/platform/v1/campaigns/lbm_p7q2zx' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"status": "paused"}' ``` **One exception: a `rules` list replaces the whole rule set.** To change one rule, send them all. Read the campaign, edit the rules array, send it back. A partial rules list is not a partial update — it is a smaller rule set. **Echo each rule's `id` back.** A rule sent with the `id` from a previous response is updated in place and keeps that ID; a rule sent without one is created fresh, and a rule missing from the list is dropped. The ID is what reporting aggregates a rule's performance on, so recreating a rule you meant to edit splits its history in two. `organization_id` is immutable. An **archived** campaign rejects every update with `409`, including a status change, and so does a `name` another live campaign already uses. ## Archiving `DELETE /platform/v1/campaigns/{campaign_id}` sets the status to `archived` rather than erasing the record, so what ran stays auditable. It responds `204`. There is no un-archive. Use `paused` for anything you might want back. Archiving releases the campaign's name, so a new campaign may reuse it. ## Listing ```bash curl -G 'https://api.eu1.luigisbox.ai/platform/v1/campaigns' \ --data-urlencode 'organization_id=lbo_r2vn8c' \ --data-urlencode 'status=active' \ --data-urlencode 'intent=search' \ --data-urlencode 'query=spring' \ --data-urlencode 'sort=starts_at:desc' \ -H 'Authorization: Bearer ' ``` | Filter | Matches | |---|---| | `organization_id` | Owning organization | | `status` | Lifecycle state | | `query` | Case-insensitive substring of the name | | `effect_type` | Campaigns containing a rule with this effect. Repeatable. | | `intent` | Intents the campaign can **actually** serve right now. Repeatable. | Filters combine AND across facets and OR within a repeatable one. `intent` matches against the intents of the surfaces currently in the campaign's scope, not the stored `intents` array. A catalog-wide campaign matches every intent its catalogs host, and a campaign whose targeted surfaces were deleted matches none — the same answer [effective scope](/merchandising/business-rules/targeting/#reading-a-saved-campaigns-scope) gives. `sort` takes `:`, with the field one of `name`, `status`, `rules_count`, `starts_at` or `updated_at`, and the direction `asc` or `desc`. The response is the standard [paginated envelope](/api-basics/requests-and-responses/#pagination) plus `facet_counts.status`, a breakdown by status that ignores the `status` filter — so a UI can show "12 active, 3 paused" alongside a filtered list. ## Limits | | | |---|---| | Rules per campaign | 1–50 | | Campaign and rule name | 1–256 characters | | Condition tree depth | 10 | | Conditions per tree | 100 | ## See also - [Business rules overview](/merchandising/business-rules/overview/) — the concepts these fields express - [Targeting](/merchandising/business-rules/targeting/) · [Triggers](/merchandising/business-rules/triggers/) · [Rules and effects](/merchandising/business-rules/rules-and-effects/) - [API Reference](/api/) — the generated contract for every endpoint above