From 580353659a0b8bbe51a0d2c4b07281a1a0b88a03 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Mon, 26 Oct 2026 12:54:39 +0700 Subject: [PATCH 1/3] Campaigns: Admin API wire behavior, all_packages on tier offers, shipping offers Documents what integrations trip on when provisioning over the Admin API: package create answers with a list (one package per variant), one package per variant per campaign, offer condition PATCH replaces while benefit PATCH keeps price_rounding, the offers list omits packages, shipping PATCH ignores a bare price, and ids are numbered per campaign. Concepts gains the all_packages warning for tier offers, shipping discounts as offers, one language per campaign, the fixed default currency, and the browser-safe campaign key. --- content/docs/campaigns/admin-api/index.mdx | 32 ++++++++++++++++++++-- content/docs/campaigns/index.mdx | 8 ++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/content/docs/campaigns/admin-api/index.mdx b/content/docs/campaigns/admin-api/index.mdx index ca7c9aa..8863733 100644 --- a/content/docs/campaigns/admin-api/index.mdx +++ b/content/docs/campaigns/admin-api/index.mdx @@ -60,7 +60,7 @@ Campaign endpoints need API version `2024-04-01` or later. ### Clone a Campaign -- Copies settings, packages and prices, shipping methods, and offers. +- Copies settings, packages and prices, shipping methods, and offers, including their codes. - Package and shipping method IDs stay the same, so existing pages only need the new `api_key`. - The copy is named `-COPY`. Rename it with [campaignsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsPartialUpdate). @@ -74,6 +74,8 @@ A package is the campaign's sellable reference to a product or variant. Its `id` - Only `name` and `product_id` are required. - Create packages before the offers that use them. +- The response is a list with one package per variant in `product_variant_ids`, so a single variant comes back as a one-element list. Read each package `id` from that list. +- Each variant can back only one package per campaign. A second package for the same variant returns `400`. Sell it at another price with an offer. - The dashboard creates packages when you pick products in an offer. Call [campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) first to avoid duplicates. ```json title="Create Package" http-method="POST" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/packages/" @@ -203,6 +205,7 @@ flowchart TD ``` - Create one offer per quantity tier. +- List the tier's `package_ids`. `all_packages: true` also covers packages added later, so a tier offer that uses it discounts full-price add-ons too. - Only one automatic offer of each benefit type applies. [Concepts → Offers](/docs/campaigns#offers) explains which one wins. - `"available": false` creates the offer switched off. Turn it on with [campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate). @@ -228,9 +231,26 @@ Automatic offers run only at checkout. Upsell, downsell, and exit-pop discounts ``` -`package_ids` is write-only. The response returns a `packages` array instead. +`package_ids` is write-only. [campaignsOffersRetrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) returns a `packages` array instead. [campaignsOffersList](/docs/admin-api/reference/campaigns/campaignsOffersList) leaves `packages` and `all_packages` out of each condition, so retrieve an offer to see which packages it covers. +### Update an Offer + +[campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate) treats `condition` and `benefit` differently: + +- `condition` replaces the whole condition. Send `type`, `value` when the type needs one, and `package_ids` or `all_packages: true` every time. The packages you leave out are removed from the offer. +- `benefit` needs `type` and `value`. Leave out `price_rounding` to keep the current rounding, or send `null` to remove it. +- Fields you leave out of the top level, such as `name` or `code`, don't change. + +```json title="Raise a Tier Discount" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/offers/{offerId}/" +{ + "benefit": { + "type": "package_percentage", + "value": "35.00" // price_rounding is left as it was + } +} +``` + ### Add Shipping Methods to a Campaign A campaign shipping method sets a campaign price for a shipping method configured on the store. `shipping_method` and `price` are required. @@ -245,7 +265,9 @@ A campaign shipping method sets a campaign price for a shipping method configure } ``` -The campaign's other currencies are converted from this price. To change prices later, [campaignsShippingMethodsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) takes the same `prices` array and `recalculate_prices` flag as [packages](#repricing-across-currencies). +The campaign's other currencies are converted from this price. To change prices later, [campaignsShippingMethodsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) takes the same `prices` array and `recalculate_prices` flag as [packages](#repricing-across-currencies). It ignores a bare `price` and returns `200` with the old price, so send `prices`. + +To discount shipping when an order qualifies, keep the campaign price and add a `shipping_percentage` offer instead of a second, cheaper shipping method. ```json title="Update Shipping Prices" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/" { @@ -290,6 +312,10 @@ Check `product_purchase_availability` and `product_inventory_availability` on ea Live funnel pages use the campaign's `api_key` and package IDs. Take them down or repoint them before you delete either. + +Package, offer, and shipping method `id`s start at 1 in each campaign, so the same `id` exists on many campaigns. Always send it with its campaign `id`. + + Set authorized domains in your store's Campaign Settings, not through this API. See [Concepts → Domains](/docs/campaigns#domains). diff --git a/content/docs/campaigns/index.mdx b/content/docs/campaigns/index.mdx index abe3db4..ec928f2 100644 --- a/content/docs/campaigns/index.mdx +++ b/content/docs/campaigns/index.mdx @@ -159,6 +159,10 @@ Use the version selector on that site to read the docs for the SDK version your A **campaign** bundles everything needed for a checkout: packages, offers, shipping options, payment methods, and localization. Each campaign has a unique **API key** used by the SDK to authenticate requests. +- A campaign has one language. To sell in another language, create a separate campaign. +- The default currency is fixed once the campaign is created. Additional currencies can change. +- The API key is meant for browser code. The [authorized domains](#domains) decide where it works, so it needs no secret handling. Keep Admin API tokens off your pages. + ### Packages A **package** is the campaign's sellable reference to a product or product variant. In SDK 0.4.x-compatible builds, keep packages focused on identity and base price: @@ -199,6 +203,8 @@ Every offer has a **condition** that decides when it applies and a **benefit** t A condition targets either every package on the campaign (`all_packages: true`, which also covers packages added later) or a specific list of `package_ids`. In the Campaigns App dashboard, this is the offer's **Products** selection or **All Products**. +Give quantity tier offers a `package_ids` list. If the checkout sells full-price add-ons, `all_packages` discounts them too. + #### Benefit types All benefits are percentage based. `value` is the percentage off. @@ -229,6 +235,8 @@ The package's list price stays at $49.95/unit. Each tier is a `count` condition A **shipping method** in a campaign is a virtual link to a shipping method configured on the store, with campaign-specific custom pricing. This lets you charge a different shipping rate (e.g. a flat $4.95 or free shipping) per campaign without changing the underlying store-level shipping method. +To discount shipping only when an order qualifies, such as free shipping at 3 or more, keep the campaign price and add an offer with a `shipping_percentage` benefit. Don't add a second, cheaper shipping method for it. + ### Domains Domains are configured in your store's **Campaign Settings** and apply across all campaigns. They control which domains are authorized to use your campaign API keys. From 9313d8d661d9fe24eb097b5a45428da83280a1ec Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Mon, 26 Oct 2026 13:03:59 +0700 Subject: [PATCH 2/3] Trim the campaign setup notes --- content/docs/campaigns/admin-api/index.mdx | 30 +++++++--------------- content/docs/campaigns/index.mdx | 10 +++----- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/content/docs/campaigns/admin-api/index.mdx b/content/docs/campaigns/admin-api/index.mdx index 8863733..8b57f5b 100644 --- a/content/docs/campaigns/admin-api/index.mdx +++ b/content/docs/campaigns/admin-api/index.mdx @@ -74,8 +74,8 @@ A package is the campaign's sellable reference to a product or variant. Its `id` - Only `name` and `product_id` are required. - Create packages before the offers that use them. -- The response is a list with one package per variant in `product_variant_ids`, so a single variant comes back as a one-element list. Read each package `id` from that list. -- Each variant can back only one package per campaign. A second package for the same variant returns `400`. Sell it at another price with an offer. +- The response is a list with one package per variant, even for a single variant. +- A variant can be in only one package per campaign. A duplicate returns `400`. - The dashboard creates packages when you pick products in an offer. Call [campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) first to avoid duplicates. ```json title="Create Package" http-method="POST" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/packages/" @@ -205,7 +205,7 @@ flowchart TD ``` - Create one offer per quantity tier. -- List the tier's `package_ids`. `all_packages: true` also covers packages added later, so a tier offer that uses it discounts full-price add-ons too. +- Scope tier offers with `package_ids`. `all_packages` would also discount add-ons. - Only one automatic offer of each benefit type applies. [Concepts → Offers](/docs/campaigns#offers) explains which one wins. - `"available": false` creates the offer switched off. Turn it on with [campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate). @@ -231,25 +231,15 @@ Automatic offers run only at checkout. Upsell, downsell, and exit-pop discounts ``` -`package_ids` is write-only. [campaignsOffersRetrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) returns a `packages` array instead. [campaignsOffersList](/docs/admin-api/reference/campaigns/campaignsOffersList) leaves `packages` and `all_packages` out of each condition, so retrieve an offer to see which packages it covers. +`package_ids` is write-only. [campaignsOffersRetrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) returns a `packages` array instead; [campaignsOffersList](/docs/admin-api/reference/campaigns/campaignsOffersList) omits it. ### Update an Offer -[campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate) treats `condition` and `benefit` differently: +With [campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate): -- `condition` replaces the whole condition. Send `type`, `value` when the type needs one, and `package_ids` or `all_packages: true` every time. The packages you leave out are removed from the offer. -- `benefit` needs `type` and `value`. Leave out `price_rounding` to keep the current rounding, or send `null` to remove it. -- Fields you leave out of the top level, such as `name` or `code`, don't change. - -```json title="Raise a Tier Discount" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/offers/{offerId}/" -{ - "benefit": { - "type": "package_percentage", - "value": "35.00" // price_rounding is left as it was - } -} -``` +- `condition` replaces the whole condition, so resend `package_ids` or `all_packages`. +- `benefit` needs `type` and `value`. An omitted `price_rounding` is kept; `null` clears it. ### Add Shipping Methods to a Campaign @@ -265,9 +255,7 @@ A campaign shipping method sets a campaign price for a shipping method configure } ``` -The campaign's other currencies are converted from this price. To change prices later, [campaignsShippingMethodsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) takes the same `prices` array and `recalculate_prices` flag as [packages](#repricing-across-currencies). It ignores a bare `price` and returns `200` with the old price, so send `prices`. - -To discount shipping when an order qualifies, keep the campaign price and add a `shipping_percentage` offer instead of a second, cheaper shipping method. +The campaign's other currencies are converted from this price. To change prices later, [campaignsShippingMethodsPartialUpdate](/docs/admin-api/reference/campaigns/campaignsShippingMethodsPartialUpdate) takes the same `prices` array and `recalculate_prices` flag as [packages](#repricing-across-currencies). A bare `price` is ignored. ```json title="Update Shipping Prices" http-method="PATCH" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/shipping-methods/{shippingMethodId}/" { @@ -313,7 +301,7 @@ Live funnel pages use the campaign's `api_key` and package IDs. Take them down o -Package, offer, and shipping method `id`s start at 1 in each campaign, so the same `id` exists on many campaigns. Always send it with its campaign `id`. +Package, offer, and shipping method `id`s start at 1 in each campaign. Always pair one with its campaign `id`. diff --git a/content/docs/campaigns/index.mdx b/content/docs/campaigns/index.mdx index ec928f2..38f1f3a 100644 --- a/content/docs/campaigns/index.mdx +++ b/content/docs/campaigns/index.mdx @@ -159,9 +159,7 @@ Use the version selector on that site to read the docs for the SDK version your A **campaign** bundles everything needed for a checkout: packages, offers, shipping options, payment methods, and localization. Each campaign has a unique **API key** used by the SDK to authenticate requests. -- A campaign has one language. To sell in another language, create a separate campaign. -- The default currency is fixed once the campaign is created. Additional currencies can change. -- The API key is meant for browser code. The [authorized domains](#domains) decide where it works, so it needs no secret handling. Keep Admin API tokens off your pages. +A campaign has one language, so another language needs its own campaign. The default currency can't change after create. The API key is safe in browser code; the [authorized domains](#domains) limit where it works. ### Packages @@ -201,9 +199,7 @@ Every offer has a **condition** that decides when it applies and a **benefit** t | `count` | The quantity of a matching package is at or above `value` | Buy 2 or more Widgets | | `count_equal` | The quantity of a matching package is exactly `value` | Exactly 2 Widgets | -A condition targets either every package on the campaign (`all_packages: true`, which also covers packages added later) or a specific list of `package_ids`. In the Campaigns App dashboard, this is the offer's **Products** selection or **All Products**. - -Give quantity tier offers a `package_ids` list. If the checkout sells full-price add-ons, `all_packages` discounts them too. +A condition targets either every package on the campaign (`all_packages: true`, which also covers packages added later) or a specific list of `package_ids`. In the Campaigns App dashboard, this is the offer's **Products** selection or **All Products**. Use `package_ids` for tier offers, since `all_packages` would also discount full-price add-ons. #### Benefit types @@ -235,7 +231,7 @@ The package's list price stays at $49.95/unit. Each tier is a `count` condition A **shipping method** in a campaign is a virtual link to a shipping method configured on the store, with campaign-specific custom pricing. This lets you charge a different shipping rate (e.g. a flat $4.95 or free shipping) per campaign without changing the underlying store-level shipping method. -To discount shipping only when an order qualifies, such as free shipping at 3 or more, keep the campaign price and add an offer with a `shipping_percentage` benefit. Don't add a second, cheaper shipping method for it. +For conditional shipping discounts, such as free shipping at 3 or more, use a `shipping_percentage` offer instead of a second shipping method. ### Domains From 71671828c8e27165a5d049fbaed71fbc55a1cfe5 Mon Sep 17 00:00:00 2001 From: Devin Michael Date: Mon, 26 Oct 2026 13:19:27 +0700 Subject: [PATCH 3/3] Restore precision Kilo flagged in the trimmed notes --- content/docs/campaigns/admin-api/index.mdx | 8 ++++---- content/docs/campaigns/index.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/campaigns/admin-api/index.mdx b/content/docs/campaigns/admin-api/index.mdx index 8b57f5b..3970342 100644 --- a/content/docs/campaigns/admin-api/index.mdx +++ b/content/docs/campaigns/admin-api/index.mdx @@ -74,8 +74,8 @@ A package is the campaign's sellable reference to a product or variant. Its `id` - Only `name` and `product_id` are required. - Create packages before the offers that use them. -- The response is a list with one package per variant, even for a single variant. -- A variant can be in only one package per campaign. A duplicate returns `400`. +- The response is a list with one package per variant, even for a single variant. Read each `id` from the list; the reference shows a single object. +- A variant can be in only one package per campaign. A duplicate returns `400`. Sell it at another price with an offer. - The dashboard creates packages when you pick products in an offer. Call [campaignsPackagesList](/docs/admin-api/reference/campaigns/campaignsPackagesList) first to avoid duplicates. ```json title="Create Package" http-method="POST" http-target="https://{store}.29next.store/api/admin/campaigns/{id}/packages/" @@ -231,14 +231,14 @@ Automatic offers run only at checkout. Upsell, downsell, and exit-pop discounts ``` -`package_ids` is write-only. [campaignsOffersRetrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) returns a `packages` array instead; [campaignsOffersList](/docs/admin-api/reference/campaigns/campaignsOffersList) omits it. +`package_ids` is write-only. [campaignsOffersRetrieve](/docs/admin-api/reference/campaigns/campaignsOffersRetrieve) returns a `packages` array instead. [campaignsOffersList](/docs/admin-api/reference/campaigns/campaignsOffersList) omits `packages` and `all_packages`, so retrieve an offer to see its scope. ### Update an Offer With [campaignsOffersPartialUpdate](/docs/admin-api/reference/campaigns/campaignsOffersPartialUpdate): -- `condition` replaces the whole condition, so resend `package_ids` or `all_packages`. +- `condition` replaces the whole condition, so resend `type`, `value` when the type needs one, and `package_ids` or `all_packages: true`. - `benefit` needs `type` and `value`. An omitted `price_rounding` is kept; `null` clears it. ### Add Shipping Methods to a Campaign diff --git a/content/docs/campaigns/index.mdx b/content/docs/campaigns/index.mdx index 38f1f3a..ab1a113 100644 --- a/content/docs/campaigns/index.mdx +++ b/content/docs/campaigns/index.mdx @@ -159,7 +159,7 @@ Use the version selector on that site to read the docs for the SDK version your A **campaign** bundles everything needed for a checkout: packages, offers, shipping options, payment methods, and localization. Each campaign has a unique **API key** used by the SDK to authenticate requests. -A campaign has one language, so another language needs its own campaign. The default currency can't change after create. The API key is safe in browser code; the [authorized domains](#domains) limit where it works. +A campaign serves one language at a time, so another language needs its own campaign. The default currency can't change after create. The API key is safe in browser code; the [authorized domains](#domains) limit where it works. ### Packages