---
title: "Update experience"
method: PUT
path: "/v25-10-beta/experiences/{experienceId}"
tags: ["Update Experience"]
---

# Update experience

`PUT /v25-10-beta/experiences/{experienceId}`

## Request

- Base URL: `https://api.intelligems.io`
- URL: `https://api.intelligems.io/v25-10-beta/experiences/{experienceId}`
- Auth: API key in header `intelligems-access-token`
- TypeScript: <https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/docs/v25-10-beta/experiences/:experienceId/put.ts> (a function with typed inputs; its response is checked against the schema)

Updates an existing experience by ID. This is a full PUT replacement — the entire experience configuration is replaced with the provided data.

All required fields must be included in every request. Any optional fields omitted will be reset to their defaults.

### Required Fields (inside `experience`)

- `id` (UUID) — ID of the experience to update
- `name` (string) — Name of the experience
- `type` (string) — Send back the value from the GET response. A content test can switch between `"content/advanced"`, `"content/onsiteEdits"`, `"content/url"`, `"content/theme"` and `"content/template"`. An `"onsiteUpsell"` test keeps that type. A personalization keeps its stored `"personalization"` or `"personalization/offer"`; the only change is that a `"personalization/offer"` whose offer is removed becomes `"personalization"`.
- `experienceAnalysis` — Analysis configuration (includes `analyticsViewType`)
- `variations` — Array of variation objects. Each variation must include its `id`.
- `experienceKeyMetrics` — Array of key metric objects. Include `id` to preserve an existing metric; omit it to add a new one (an id is generated automatically). To attach a custom event as a key metric, set `customEventId` to an id from the custom events API — the per-experience custom-metric link is created automatically.

### Optional Fields (inside `experience`)

- `description` (string) — Description of the experience
- `currency` — Currency display settings
- `previewPath` (string) — URL path for previewing
- `audience` — Audience targeting rules (must include `id` if provided)
  - `excludeCurrency` — Currency/geo-based visitor targeting. When `exclude` is
    `true`, only visitors matching the given `currency` (ISO 4217 code), are included in the experience; everyone
    else is excluded. Set `currency` to `null` (with `exclude: false`) to disable.
- `experiencePageTargeting` — Array of page targeting rules (each must include `id`)

### Important Notes

- The experience must exist and belong to your organization
- Updating a running experience may affect active visitors
- Variation weights must sum to 100
- Variations, page targeting, and audience filters must include their `id` field. Key metrics are the exception: omit `id` to add a new one, since a new metric has no existing id to reference.
- Custom events already attached to the experience (`experienceCustomMetrics`) are kept on every update, whether or not they are key metrics. Removing a key metric does not detach its custom event.
- A personalization follows the same rules on update as on create: one variation, a show/hide audience, and the supported changes. What this request can't carry — product targeting, offer widgets, checkout blocks, and where the personalization came from — is kept from the stored experience.
- Unlike the rest of the body, an offer's `products` is kept when you leave it out. Send `[]` to remove every product, so the offer covers every product again, or send the whole new list to replace it. A different offer on a variation (a new `id`) starts with no products. Read the current list with `GET /experiences/{experienceId}/offer-products`; its entries' `id`, `set` and `variants` can be edited and sent back. See Create experience for the fields.
- An update never turns a test into a personalization or a personalization into a test.
- Checkout blocks and post-purchase upsells can't be sent in this body, and an update keeps the ones already on each variation it keeps. Add, change, and delete them with `/v25-10-beta/experiences/{experienceId}/checkout-blocks`.
- Onsite upsells are the exception to the full replace: a variation that leaves `components` out keeps its stored upsells, and one that sends the field replaces them with the list sent (an empty list removes them). To change an upsell, send it back with its `id` from the GET response and the whole `config`; the Agentic Logic placement it is bound to stays attached. Leave `template` out to keep its current template. Without an `id` it is a new upsell, with the same checks as on create (storefront access token, plan scope, no new bundles). An `id` that isn't on that variation is rejected.

**Rate Limit:** 50 requests per 30s window, refilling 25 tokens every 30s.

## Path parameters

- `experienceId` string, required

## Request body

- object
  - `experience` object, required: The complete experience object containing settings, variations, audience, page targeting, and key metrics. For PUT: include id fields from the GET response on all nested entities.
    - `name` string, required: Short display name shown in dashboards and reports. Aim for under ~60 characters. Summarize rather than enumerate — do NOT list every product. For 3 or more products, describe the set (e.g. '+10% price test — 5 bestsellers' or '+$3 price test — Watches collection'), not the individual titles.
    - `description` string, nullable: Internal note explaining what this experience tests. Helpful for team context. Free-text and human-owned — it is never regenerated automatically, so if you later add products to this experience, update it yourself if it has gone stale.
    - `currency` object, nullable: Currency for price display and analytics. For a PRICE TEST this selects the TEST currency and visitor currency targeting is locked to it, so only shoppers browsing in that currency enter the test. On CREATE, omit it to run in the store's default currency. `code` is a required ISO 4217 code (e.g. 'GBP') whenever the object is sent; `symbol` is derived from the code unless you supply one.
      - `code` string, required: ISO 4217 currency code used for analytics and price calculations.
      - `country` string: ISO country code for locale-specific formatting.
      - `options` unknown
      - `symbol` string: Display symbol prepended to amounts (e.g., '$', '€').
      - `suffix` string: Text appended after amounts for postfix currencies (e.g., 'kr' in '100 kr').
    - `previewPath` string, nullable: Store URL path for generating preview links (e.g., '/products/example').
    - `type` 'content/advanced' | 'content/onsiteEdits' | 'content/url' | 'content/theme' | 'content/template' | 'personalization' | 'personalization/offer' | 'pricing' | 'checkoutBlock' | 'postPurchase' | 'onsiteUpsell', required: Send back the value from the GET response. A content test can switch to another content type (content/url, content/template, content/theme, content/advanced, content/onsiteEdits). A personalization keeps its stored type (personalization or personalization/offer); the only change is that a personalization/offer whose offer is removed becomes personalization. Any other type change is rejected.
    - `experienceAnalysis` object, required: Controls which orders are included in analytics results.
      - `analyticsViewType` 'test_orders_only' | 'sitewide_orders' | 'none': Scope of orders included in analytics results. Determines whether only tested products or all store orders are counted.
    - `productVariantAction` union
      - object
        - `type` 'analytics', required
        - `location` object, required
          - `bucket` string, required
          - `key` string, required
      - object
        - `type` 'price', required
        - `location` object, required
          - `bucket` string, required
          - `key` string, required
      - object
        - `type` 'offer', required
        - `location` object, required
          - `bucket` string, required
          - `key` string, required
    - `id` string, uuid, required: Copy from GET response. Identifies which experience to update.
    - `variations` object[], required: Full replacement — include ALL variations to keep. Omitted variations are deleted. One must be control, percentages must sum to 100. A personalization has exactly one variation: the control, at 100%. Match variation mechanisms to experience type: onsiteEdits for DOM changes, onsiteInjection for CSS/JS, redirects for URL/theme/template tests. On a personalization a redirect is on that one variation and fires for everyone it targets; a theme and a template redirect can't share one.
      - `name` string, required: Display name (e.g., 'Control', 'Variant A'). Must be unique across variations.
      - `percentage` number, required: Traffic allocation weight. All percentages across variations must sum to 100.
      - `isControl` boolean: The baseline variation all others are measured against. Exactly one per experience.
      - `order` integer, required: Display order in analytics. Must be unique across variations. 0-indexed.
      - `priceChange` string, nullable: Group-level price transform for a pricing-type experience. Set on each non-control test group to define its price change relative to each product's base price (e.g. '10' with priceChangeUnit 'percent' for +10%, or '-5' for a discount). Leave null/empty on the control group so it keeps the base price. This defines the group's transform only; NO products are attached here. REQUIRED NEXT STEP: after this create returns an experienceId, call prepare_seed_price_test_products with that id and the chosen product variants to attach them and compute per-variant prices — it reads this transform back off the experience. A pricing experience created here has no products until you run that seed step. On a PERSONALIZATION, set it on the one variation (the control) instead, and it must be negative, since a personalization can only lower prices; then run the same seed step.
      - `priceChangeUnit` 'percent' | 'dollar', nullable: How priceChange is applied: 'percent' (a percentage of the base price) or 'dollar' (a fixed monetary amount).
      - `priceRoundingType` 'none' | 'nearest' | 'up' | 'down', nullable: Rounding applied after priceChange: 'none', 'nearest', 'up', or 'down' (e.g. 'down' with priceRoundingAmount '0.99' for charm pricing).
      - `priceRoundingAmount` string, nullable: Target value for rounding, e.g. '0.99' for charm pricing like $X.99. Only meaningful when priceRoundingType is not 'none'.
      - `excludeProducts` boolean: Read-only in practice: always false, so leave it out. An offer covers only the products it lists, so there's no flag for leaving products out; to leave some variants of a listed product out, mark them inclusionType 'exclude'. A value other than false is rejected, except the value the variation already has.
      - `includeProducts` boolean: Read-only in practice: it follows the products on this variation's offer, so leave it out. True when the offer lists products with set 'buy', false otherwise. To limit an offer to products, send them in products on the offer. A value that disagrees with the list is rejected, except the value the variation already has. A price test doesn't use it: its products are set through the price endpoint.
      - `id` string, uuid, required: Copy from GET response. Omitting a variation from the array deletes it.
      - `onsiteEdits` object[]: Full replacement — include ALL edits to keep. Omitted edits are deleted. ONLY for DOM content changes (text replacement, HTML insertion, image swaps, element removal). Do NOT create onsiteEdits for visual styling changes like colors, backgrounds, fonts, or layout — use onsiteInjection.customCss instead. Leave as empty array when only CSS/JS changes are needed.
        - `onsiteEditType` 'html' | 'text' | 'image' | 'hide', nullable: Content modification strategy (rich HTML, plain text, image swap, or element removal).
        - `find` string, nullable, required: Pattern to find on the page. Set isRegex=true for regex. Null when using querySelectors with 'hide' or 'image' type.
        - `title` string, nullable: Dashboard label for organizing multiple edits.
        - `isRegex` boolean: Set to true to interpret 'find' as a regex pattern.
        - `replace` string: Replacement content: HTML markup for 'html' type, plain text for 'text', image URL for 'image'.
        - `querySelectors` string[]: CSS selectors scoping where the edit applies (e.g., '.product-title').
        - `skip` boolean: Set to true to temporarily disable without deleting.
        - `remove` boolean: Set to true to remove the matched element entirely instead of replacing.
        - `groupId` string: Edits sharing a groupId are managed together as a unit.
        - `insertType` 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', nullable, required: DOM insertion position relative to target element. Null for content replacement (default).
        - `imageSearchUrl` string, nullable: URL pathname to match for image replacement. Mutually exclusive with querySelectors for image edits.
        - `source` 'ai', nullable
        - `id` string, uuid, required: Copy from GET response. Identifies which onsite edit to preserve.
        - `variationId` string, uuid, required: Copy from GET response. Must match the parent variation's id.
      - `onsiteInjection` object, nullable: Full replacement. Set to null to remove. Use for all visual styling changes (background-color, font-size, colors, borders, layout, etc.) via customCss, and custom behavior via customJs. CSS-only changes should use ONLY this field with an empty onsiteEdits array — do not create a corresponding onsiteEdit.
        - `customCss` string, nullable: CSS injected globally on the page when this variation is active.
        - `customJs` string, nullable: JavaScript executed on the page. Execution timing controlled by jsInjectionMode.
        - `jsInjectionMode` object, required: Controls when customJs runs: immediately on load, after window load, or after a configurable delay.
          - `type` 'immediately' | 'onWindowLoad' | 'timeout': Execution timing for injected JS. When 'timeout', the timeout field specifies the delay.
          - `timeout` union: Delay in milliseconds before JS execution. Only relevant when type is 'timeout'.
            - number
            - string
        - `source` 'ai', nullable
        - `id` string, uuid, required: Copy from GET response. Identifies which injection to update.
        - `variationId` string, uuid, required: Copy from GET response. Must match the parent variation's id.
      - `offer` object, nullable
        - `enabled` boolean: Whether the offer is actively applying discounts to customers on the storefront.
        - `isArchived` boolean: Whether the offer has been archived and is no longer active.
        - `name` string, required: Internal label for identifying the offer in dashboards.
        - `unitType` 'unit' | 'dollar': Threshold basis for tiered discounts: per-item quantity or monetary amount spent.
        - `discountType` 'percentage' | 'dollar': How the discount value is calculated (e.g., percentage off, fixed amount off).
        - `tiers` object[]
          - `id` string, uuid
          - `minimumUnits` number
          - `maximumUnits` number: Upper bound of the tier, which must be at least minimumUnits. The discount only checks the minimum, so for an open-ended tier ("3 or more") send 1000000000.
          - `unitDiscount` number
          - `bxgyGetQuantity` integer
          - `discountTitle` string, nullable
          - `giftWithPurchaseTitle` string, nullable
          - `isFreeShipping` boolean
          - `isGiftWithPurchase` boolean
          - `giftWithPurchaseProductId` string, nullable: Free gift only: the gift's Shopify product id, the number the Shopify admin shows or gid://shopify/Product/<n>. Saved as the number.
          - `giftWithPurchaseVariantId` string, nullable: Deprecated: use giftWithPurchaseVariantIds.
          - `giftWithPurchaseVariantIds` string[], required: Free gift only: the gift's variants shoppers can get, as numbers or gid://shopify/ProductVariant/<n>. Empty for every variant.
          - `autoAddGiftWithPurchase` boolean
          - `giftWithPurchaseHandle` string, nullable
        - `maximumDollarDiscount` number, nullable: Hard cap on the total dollar discount. Null means no cap is applied.
        - `discountApplicationType` 'tieredDiscount' | 'cartDiscount' | 'freeGift' | 'freeShipping' | 'buyXGetY', required: Core discount mechanic (tiered volume, flat cart-level, gift-with-purchase, or free shipping).
        - `discountApplicationMethod` 'perItem' | 'perOrder', required: Whether the discount applies per individual item or to the entire order.
        - `combinesWithIntelligems` boolean, required: Whether this offer stacks with other active Intelligems discounts.
        - `combinesWithShopify` boolean, required: Whether this offer stacks with native Shopify discount codes.
        - `shouldRejectCodes` boolean, required: When true, manual discount codes entered at checkout are rejected while this offer is active.
        - `shippingDiscount` object, nullable: Only for discountApplicationType 'freeShipping', and then required: a free shipping offer only discounts shipping when it has this plus one tier with isFreeShipping true. discountType must be 'percentage', and unitDiscount is the percent off shipping, from above 0 up to 100 (100 is free shipping). Dollar-amount shipping discounts are rejected. Copy its id from the GET response when updating, like the offer's id. Leave it out for a new offer; the server assigns one.
          - `id` string, uuid
          - `discountType` 'percentage', required
          - `unitDiscount` number, required: Percent off the shipping rate. 100 is free shipping.
          - `rateFilter` union, required
            - object
              - …
            - object
              - …
          - `countryFilter` object, nullable, required
            - `allow` string[]
            - `exclude` string[]
          - `discountLabel` string, nullable, required
        - `requireSameItem` boolean: For tiered discounts: when true, only multiples of the same product count toward thresholds.
        - `bxgyGetProductScope` 'all' | 'list', nullable: Buy X Get Y only: where the discounted units come from. null for Same products: shoppers get the discount on the products they buy. 'list' for Different products from a list: send those products in products with set 'get'. 'all' for Different products from any product, with no get list. Leaving it out keeps the stored value (null on create), except when the request sends products: get products set it to 'list', and a list without them turns a stored 'list' into 'all'.
        - `subscriptionApplicationType` 'oneTime' | 'firstSub' | 'oneTimeAndFirstSub': Controls which purchase types this offer applies to: 'oneTime' (one-time purchases only), 'firstSub' (first subscription orders only), or 'oneTimeAndFirstSub' (both one-time and first subscription orders). Defaults to 'oneTime'. Free shipping offers (discountApplicationType 'freeShipping') always apply to both, so leave this out or send 'oneTimeAndFirstSub' for them. Any other value on a free shipping offer is rejected.
        - `products` object[]: The products this offer applies to, one entry per product. Leave it out on create for an offer on every product. On update, leaving it out keeps the stored list, [] removes every product so the offer covers every product again, and a list replaces the stored one. The offer covers only its 'buy' products: there's no way yet to cover every product except a few, so list the products to include. Not allowed on freeShipping, which always covers the whole order. For freeGift the list is the products that qualify, not the gift. The gift goes on a tier: isGiftWithPurchase true and giftWithPurchaseProductId set to the gift's product id, with giftWithPurchaseVariantIds listing the variants shoppers can get (empty for all) and autoAddGiftWithPurchase true to add a one-variant gift to the cart by itself. Keep that tier's unitDiscount at 0.
          - `id` string, required: Shopify product id: the bare number the Shopify admin shows, or gid://shopify/Product/<n>. Titles, variants and prices are looked up from the store, so only ids are sent.
          - `set` 'buy' | 'get': 'buy' (the default) for a product the offer applies to. 'get' only on a Buy X Get Y offer with bxgyGetProductScope 'list': a product shoppers get at the discount. A product can be in both sets.
          - `variants` object[]: Which of this product's variants the offer covers. Leave it out, or send [], for every variant, including variants added to the product later. If any entry is 'include', the offer covers only the included variants. If every entry is 'exclude', it covers every variant except those. With any variant left out, variants added to the product later aren't covered. At least one variant has to stay covered.
            - `id` string, required: Shopify variant id of this product: the bare number the Shopify admin shows, or gid://shopify/ProductVariant/<n>.
            - `inclusionType` 'include' | 'exclude': 'include' (the default) or 'exclude'. See variants on the product for how a list of them is read.
        - `id` string, uuid: Copy from GET response to update that offer. Leave it out for a new offer; the server assigns one. An id this experience doesn't already have is replaced with a new one.
        - `variationId` string, uuid: Optional. The offer belongs to the variation it's nested in, whatever is sent here.
      - `redirects` object[]
        - `group` number, required: Redirects sharing the same originUrl should use the same group number.
        - `originUrl` string, required: URL pattern to match. Matching behavior controlled by 'filter' field.
        - `destinationUrl` string, nullable: Target URL. Required when redirectType is 'fullReplacement'.
        - `queryParams` object[]: Appended to the destination URL. On a theme redirect this is where the theme goes: [{ key: "preview_theme_id", value: "<theme id>" }]. On a template redirect this is where the template goes, and it is what the server checks: [{ key: "view", value: "<templateType>.<suffix>.json" }], e.g. "product.alternate.json".
          - `key` string, required: Query parameter name appended to the destination URL.
          - `value` string, required: Query parameter value.
        - `skip` boolean, required: Set to true to temporarily disable without deleting. On a split URL test (type 'content/url') the control's entry is ALWAYS `skip: true` with `destinationUrl: null` but the SAME `originUrl`/`filter`/`find` as the treatment — the plugin matches every entry's originUrl to decide who is in the test, so control needs a matching entry that stays put. A missing or non-skipped control entry is corrected server-side. On any other experience type a redirect belongs to its own variation and keeps the `skip` you send, including on the control.
        - `isThemeTest` boolean, required: Set to true for Shopify theme testing. Use with content/theme experience type.
        - `isTemplateTest` boolean, required: Set to true for Shopify template testing. Use with content/template experience type.
        - `templateType` '404' | '' | 'article' | 'blog' | 'cart' | 'collection' | 'index' | 'list-collections' | 'page' | 'password' | 'product' | 'search', nullable: Required when isTemplateTest is true. The Shopify template type the redirect applies to (product, collection, page, ...). No tool lists a theme's template files, and the server doesn't check that the one in the "view" param exists, so ask the user for the template name instead of guessing one.
        - `templateSuffixes` string[], nullable: The template files visitors are enrolled from, in the same form as the "view" value. On a test, the "view" values of the group. On a personalization, the template visitors are on first, then the one the redirect sends them to: ["templates/product.json", "product.alternate.json"]. The server checks the "view" query param, not this list.
        - `redirectOnce` boolean: Set to true to fire only once per visitor session.
        - `filter` 'matchesExactly' | 'contains' | 'endsWith' | 'matchesRegex' | 'none': How originUrl is matched against the current page URL.
        - `find` string, nullable: Pattern to find in the URL. Required when redirectType is 'partialReplacement'.
        - `redirectType` 'fullReplacement' | 'partialReplacement': Full replacement navigates to destinationUrl; partial replacement swaps only the 'find' match within the current URL.
        - `id` string, uuid, required: Copy from GET response. Identifies which redirect to preserve.
        - `variationId` string, uuid, required: Copy from GET response. Must match the parent variation's id.
      - `components` object[]: The onsite upsells on this variation. Omit the field to keep the stored ones; send the full list to replace them, and an empty list to remove them all. Each entry's `config` is the whole upsell: where it renders (`placement`), its markup (`template`, omit for the default), which products it suggests (`rules[].action`) and when it shows (`rules[].targeting`). On a test the experience type must be `onsiteUpsell`, and the control normally has none; on a personalization it goes on the one variation. The store needs a storefront access token and the onsite upsell plan scope. A bundle (`config.type` `bundleOnsiteUpsell`) is in beta: one the app made can be sent back, but a new one is rejected.
        - `config` union, required: The whole upsell: its type, placement, template, product rules with their targeting, and discount. Sent as a unit, so include every field to keep. A stored upsell sent back with its id keeps three things the request leaves out: its template (when the type is the same), its spot when placement has only the same location, and its rule ids by position.
          - object
            - `type` 'onsiteUpsell', required: Discriminator for the component kind.
            - `placement` object, required: Where the upsell renders. `location` (pdp or cart) is required. Send it alone on a new upsell for the spot the app starts one with: right after the add-to-cart form on a product page, or just above the cart footer. On a stored upsell sent back with its id, `location` alone keeps its current spot if the location is the same, and moves it to that default spot if the location changed. Or add `anchorKey`, the key of an Intelligems Content theme block the merchant has placed, or `selector` with `insertType` for a spot of your own, but not both.
              - …
            - `template` string: HTML for the upsell, including all static copy written inline. Omit it on a new upsell to use the Intelligems default template, the one the app starts from, or on a stored upsell sent back with its id to keep its current template. The per-product card is the markup between {{#each products}} and {{/each}}. Use every slot and no others: {{product.image}}, {{product.url}}, {{product.title}}, {{product.price}}, {{product.compareAtPrice}}, {{variant-select}} and {{atc}}. Each one hides itself when it has nothing to show, so keep all seven even where you don't want that element. Everything else is literal HTML.
            - `maxProductLimit` integer, required: Maximum number of products to render.
            - `excludeOutOfStock` boolean: Exclude products whose variants are all out of stock. Defaults to true.
            - `excludeItemsInCart` boolean: Exclude products already in the shopper's cart. Defaults to true.
            - `excludePdpProduct` boolean: On a PDP, exclude the product being viewed. Defaults to true; turn it off when the viewed product belongs in the result, e.g. a bundle built around it.
            - `discount` object: Optional discount applied to products added from this upsell.
              - …
            - `rules` object[], required: Prioritized list of rules. Each pairs an action (which products) with targeting (when to show). v1 ships a single rule.
              - …
          - object
            - `type` 'bundleOnsiteUpsell', required: Discriminator for the component kind.
            - `placement` object, required: Where the upsell renders. `location` (pdp or cart) is required. Send it alone on a new upsell for the spot the app starts one with: right after the add-to-cart form on a product page, or just above the cart footer. On a stored upsell sent back with its id, `location` alone keeps its current spot if the location is the same, and moves it to that default spot if the location changed. Or add `anchorKey`, the key of an Intelligems Content theme block the merchant has placed, or `selector` with `insertType` for a spot of your own, but not both.
              - …
            - `template` string: HTML for the upsell, including all static copy written inline. Omit it on a new upsell to use the Intelligems default template, the one the app starts from, or on a stored upsell sent back with its id to keep its current template. The per-product card is the markup between {{#each products}} and {{/each}}. Use every slot and no others: {{product.image}}, {{product.url}}, {{product.title}}, {{product.price}}, {{product.compareAtPrice}}, {{variant-select}} and {{atc}}. Each one hides itself when it has nothing to show, so keep all seven even where you don't want that element. Everything else is literal HTML.
            - `copy` object, required: Shopper-facing text, edited in the panel rather than inline in the template.
              - …
            - `maxProductLimit` integer, required: Number of products the bundle offers.
            - `allowItemSelection` boolean: Whether the shopper can choose which of the bundle's products to add. When false every rendered product is included; exclusions can still trim how many render, so lower tiers stay reachable.
            - `excludeOutOfStock` boolean: Exclude products whose variants are all out of stock. Defaults to true.
            - `excludeItemsInCart` boolean: Exclude products already in the shopper's cart. Defaults to true.
            - `excludePdpProduct` boolean: On a PDP, exclude the product being viewed. Defaults to true; turn it off when the viewed product belongs in the result, e.g. a bundle built around it.
            - `discount` object: Optional count-tiered discount applied to the products added from this bundle.
              - …
            - `rules` object[], required: Prioritized list of rules. Each pairs an action (which products) with targeting (when to show). v1 ships a single rule.
              - …
        - `id` string, uuid: Copy from the GET response to keep a stored upsell, with the Agentic Logic placement it is bound to. Omit to add a new one; the id is generated.
    - `audience` object, nullable: Visitor targeting rules. Omit it, or send null as get_experience returns it, for no audience targeting; on update that removes any audience the experience has. Each entry in `filters` is { priority, action, filterType, expression }, where `expression` carries the match. IMPORTANT: the actions depend on the category. For a test (A/B test, including a price test) a matching filter's action must be 'randomVariation' or 'assignVariation'; 'experienceInclude' is rejected. For a personalization every filter action and the else action must be 'experienceInclude' (show it), 'experienceExclude' (hide it) or 'experienceUnassigned'; 'randomVariation' and 'assignVariation' are rejected because a personalization never splits traffic. DEVICE TARGETING for a price test — 'desktop only' — is the audience: { enabled: true, audienceType: 'custom', elseAction: 'assignVariation', elseVariationIndex: '0', elseExcludeFromAnalytics: true, filters: [{ priority: 0, action: 'randomVariation', filterType: 'device', expressionType: 'common', expression: [{ query: { key: 'device', value: 'desktop', filter: 'equals', type: 'device' } }] }] }. value is 'any' | 'mobile' | 'desktop'; the else* fields send everyone NOT on that device to the control group (index '0') and out of analytics, which is what makes it 'desktop only'.
      - `enabled` boolean, required: Set to true to enforce filter rules. When false, all visitors are included.
      - `audienceType` 'common' | 'custom' | 'advanced': Complexity level: prebuilt common rules, simple custom rules, or advanced expression trees.
      - `elseAction` 'experienceInclude' | 'experienceExclude' | 'experienceUnassigned' | 'assignVariation' | 'randomVariation' | 'leaveUnassigned', nullable, required: Fallback for visitors matching no filter: 'randomVariation' assigns randomly (most common), 'assignVariation' forces a specific variation (set elseVariationIndex), 'experienceExclude' removes from experience, 'experienceUnassigned' leaves them unassigned for now ('leaveUnassigned' is the older spelling and is stored as experienceUnassigned).
      - `elseExcludeFromAnalytics` boolean: Set to true to exclude non-matching visitors from analytics.
      - `filters` object[]: Evaluated in priority order (0-indexed). Each filter defines a targeting condition and resulting action.
        - `priority` integer, required: Evaluation order. 0-indexed. Filters with lower values are evaluated first.
        - `action` 'experienceInclude' | 'experienceExclude' | 'experienceUnassigned' | 'assignVariation' | 'randomVariation' | 'leaveUnassigned', nullable, required: What happens when a visitor matches this filter: 'assignVariation' forces a specific variation (set variationId), 'experienceExclude' removes from the experience, 'randomVariation' assigns randomly. 'experienceInclude' shows a personalization to the visitor and is valid only on personalizations, whose filters use experienceInclude, experienceExclude or experienceUnassigned and never a traffic split.
        - `filterType` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage', required: Category of targeting rule (e.g., UTM parameters, device type, visitor type).
        - `expression` object[], nullable: Boolean expression tree. Each node has either a 'query' (leaf condition) or 'operator' (logical combinator), not both.
          - `query` object
            - `key` string, required: Visitor attribute or parameter name to match (e.g., 'utm_source', 'device_type').
            - `value` string, required: Expected value to compare against the visitor's actual value for this key. Constrained by `type`: when type is 'device' this must be exactly 'any', 'mobile', or 'desktop'.
            - `filter` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', nullable: Comparison operator (e.g., equals, contains, startsWith).
            - `type` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage': Category of the parameter being matched. Determines which keys are valid. Use 'device' for device targeting (pair with key 'device', filter 'equals', and value 'any' | 'mobile' | 'desktop'); 'utm'/'url' for query params; 'urlPath' for the path; and 'country', 'visitor', 'trafficSource', 'referrer', 'cookie', 'landingPage', 'klaviyo', or 'jsExpression' for the rest.
          - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
        - `expressionType` 'common' | 'custom', nullable: Whether this uses prebuilt common rules or custom expression logic.
        - `variationId` string, uuid, nullable: Variation to assign when action is 'assignVariation'. References a variation in the parent experience.
        - `excludeFromAnalytics` boolean: When true, visitors matching this filter are excluded from analytics even if included in the experience.
        - `id` string, uuid: Server-generated. Read-only.
      - `evaluationFrequency` number, nullable: Seconds between re-evaluations for a visitor. Omit for default.
      - `elseVariationIndex` string: 0-based index into the variations array. Use instead of elseVariationId since IDs don't exist yet during creation.
      - `excludeCurrency` object: Currency and geo-based visitor exclusion configuration.
        - `exclude` boolean, required: Whether currency/geo-based exclusion is active.
        - `currency` string, nullable, required: ISO 4217 currency code to include. Null disables currency-based exclusion.
        - `country` string: ISO country code to include visitors from.
        - `region` string: Region or state code to include visitors from.
        - `city` string: City name to include visitors from.
      - `id` string, uuid: Copy from GET response. Identifies which audience config to update.
    - `experiencePageTargeting` object[]: Full replacement — include ALL page targeting rules to keep. Omitted rules are deleted. Send AT MOST ONE entry: the editor renders only the first entry that has an expression, so conditions split across several entries are invisible in the UI and are destroyed the next time anyone saves targeting there. Combine every page condition into that single entry's `expression`. That `expression` is in POSTFIX (reverse Polish) order: every condition first, then the operators combining them. An 'and'/'or' operator consumes the TWO results before it — never place an operator between two conditions. To OR three paths A, B and C send [A, B, {operator:'or'}, C, {operator:'or'}]; [A, {operator:'or'}, B, {operator:'or'}, C] is infix, is rejected, and would silently drop conditions. One condition needs no operator; N conditions need exactly N-1, all after the first two. Parentheses are not supported. Each condition is { query: { type: 'urlPath', value: '/products/some-handle', filter: 'contains', key: '' } } — 'urlPath' matches the URL path only and 'key' is unused for it. REJECTED on a split URL test (type 'content/url'): its redirects already scope the test to the pages their originUrl matches, so send an empty array and narrow the redirects' originUrl/filter instead. Other experience types, including ones whose variations carry their own redirects, accept page targeting. On a personalization with a URL or theme redirect it is dropped on save, as the app does; with a template redirect or no redirect it is kept.
      - `expression` object[], nullable
        - `query` object
          - `key` string, required: Visitor attribute or parameter name to match (e.g., 'utm_source', 'device_type').
          - `value` string, required: Expected value to compare against the visitor's actual value for this key. Constrained by `type`: when type is 'device' this must be exactly 'any', 'mobile', or 'desktop'.
          - `filter` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', nullable: Comparison operator (e.g., equals, contains, startsWith).
          - `type` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage': Category of the parameter being matched. Determines which keys are valid. Use 'device' for device targeting (pair with key 'device', filter 'equals', and value 'any' | 'mobile' | 'desktop'); 'utm'/'url' for query params; 'urlPath' for the path; and 'country', 'visitor', 'trafficSource', 'referrer', 'cookie', 'landingPage', 'klaviyo', or 'jsExpression' for the rest.
        - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
      - `id` string, uuid, required: Copy from GET response. Identifies which page targeting rule to preserve.
    - `experienceKeyMetrics` object[]: Full replacement — include ALL metrics to keep. Omitted metrics are deleted. Exactly one must be primary.
      - `order` integer, required: Display order in analytics. Must be unique across metrics. 0-indexed.
      - `isPrimary` boolean: Exactly one metric per experience must be primary.
      - `standardEventId` 'conversionRate' | 'addToCartRate' | 'abandonedCartRate' | 'abandonedCheckoutRate' | 'checkoutBeginRate' | 'checkoutEnterContactInfoRate' | 'checkoutAddressSubmittedRate' | 'viewProductPageRate' | 'viewCollectionPageRate' | 'netRevenuePerVisitor' | 'netProductRevenuePerOrder' | 'netRevenuePerOrder' | 'avgUnitsPerOrder' | 'avgProductRevenuePerUnit' | 'profitPerVisitor' | 'profitPerOrder' | 'pctOrdersFreeShipping' | 'netShippingRevenuePerOrder' | 'subscriptionOrdersPerVisitor' | 'subscriptionRevenuePerVisitor' | 'subscriptionProfitPerVisitor' | 'subscriptionProductRevenuePerOrder' | 'avgDiscountPerDiscountedOrder' | 'avgDiscountPerAllOrders' | 'pctSubscriptionOrders' | 'subscriptionRevenuePerOrder' | 'pctOrdersWithPostPurchaseOffer' | 'netRevenueFromPostPurchaseOffer' | 'pctOrdersWithUpsell' | 'netRevenueFromUpsell' | 'pctOrdersWithOnsiteUpsell' | 'netRevenueFromOnsiteUpsell', nullable: Standard e-commerce event to track. Mutually exclusive with experienceCustomMetricId -- set exactly one.
      - `experienceCustomMetricId` string, uuid, nullable: References a custom metric. Mutually exclusive with standardEventId -- set exactly one.
      - `customEventId` string, uuid: Attach an existing custom event (from list_custom_events / create_or_update_custom_event) as this key metric. The per-experience custom-metric join row is created automatically. Mutually exclusive with standardEventId and experienceCustomMetricId -- set exactly one of the three. To keep referencing an already-attached custom metric on update, pass back its experienceCustomMetricId instead (from the prior response or get_experience) -- no need to resend customEventId.
      - `id` string, uuid: Copy from GET response to preserve an existing key metric. Omit when adding a new key metric -- an id is generated automatically.

## Response `200`

OK

- object
  - `experience` object, required: Full single-experience payload returned by GET /experiences/{experienceId}. Includes `experienceCustomMetrics`, `experienceIntegrations`, `experienceKeyMetrics`, and `shippingTestMethodDefinitions`. Unlike GET /experiences-list items, this response does not include `history`.
    - `id` string, uuid, required: Server-generated. Read-only.
    - `name` string, required: Display name shown in dashboards and reports.
    - `isPreview` boolean, required: True while in draft mode. Must be set to false before the experience can be started.
    - `description` string, nullable: Internal note explaining what this experience tests.
    - `devicePreview` object, nullable: Device and path configuration for previewing the experience.
      - `device` 'any' | 'mobile' | 'desktop', required: Viewport to simulate when rendering the preview.
      - `path` string, required: Storefront URL path to load in the preview (e.g., '/products/example').
    - `organizationId` string, uuid, required: Server-generated. Read-only. The Shopify store that owns this experience.
    - `type` 'content/advanced' | 'content/onsiteEdits' | 'content/url' | 'content/theme' | 'content/template' | 'personalization' | 'personalization/offer' | 'pricing' | 'checkoutBlock' | 'postPurchase' | 'onsiteUpsell' | 'shipping' | 'sitewide', required: Determines which variation fields are relevant: content/onsiteEdits for DOM edits, content/advanced for CSS/JS injection, content/url for redirect split tests, content/theme for theme tests, content/template for template tests.
    - `category` 'experiment' | 'personalization', required: Distinguishes A/B experiments (statistical testing) from personalization rules (deterministic targeting).
    - `status` 'pending' | 'started' | 'ended' | 'paused', required: Server-managed lifecycle state. Transitions via start/pause/end actions: pending -> started -> paused/ended.
    - `testTypes` object, required: Server-generated. Read-only. Flags derived from 'type' indicating which variation capabilities are active.
      - `hasTestPricing` boolean, required: Server-generated. Read-only. True when price testing is active.
      - `hasTestShipping` boolean, required: Server-generated. Read-only. True when shipping rate testing is active.
      - `hasTestCampaign` boolean, required: Server-generated. Read-only. True when campaign/offer testing is active.
      - `hasTestContent` boolean, required: Server-generated. Read-only. True when any content variation type is enabled.
      - `hasTestContentUrl` boolean, required: Server-generated. Read-only. True when URL redirect variations are in use.
      - `hasTestContentAdvanced` boolean, required: Server-generated. Read-only. True when custom JS/CSS variations are in use.
      - `hasTestContentOnsite` boolean, required: Server-generated. Read-only. True when visual onsite edit variations are in use.
      - `hasTestContentTemplate` boolean, required: Server-generated. Read-only. True when Shopify template variations are in use.
      - `hasTestContentTheme` boolean, required: Server-generated. Read-only. True when Shopify theme variations are in use.
      - `hasTestOnsiteInjections` boolean, required: Server-generated. Read-only. True when CSS/JS injection variations are in use.
      - `hasTestCheckoutBlocks` boolean, required: Server-generated. Read-only. True when checkout block variations are in use.
      - `hasTestPostPurchase` boolean, required: Server-generated. Read-only. True when post-purchase upsell variations are in use.
      - `hasTestOnsiteUpsell` boolean, required: Server-generated. Read-only. True when onsite upsell components are in use.
    - `experiencePageTargeting` object[], required: Rules controlling where this experience activates. Primarily URL/path rules, but the same expression format also supports 'device', 'country', 'visitor', 'trafficSource', 'referrer', and 'cookie'. Empty means all pages. To target the whole experience at a device (e.g. desktop only), prefer an `audience` filter over a page-targeting rule.
      - `id` string, uuid, required
      - `filter` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', nullable
      - `filterType` 'utm' | 'url' | 'device' | 'visitor' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie', required
      - `value` string, required
      - `order` number, required
      - `expression` object[], nullable
        - `query` object
          - `key` string, required: Visitor attribute or parameter name to match (e.g., 'utm_source', 'device_type').
          - `value` string, required: Expected value to compare against the visitor's actual value for this key. Constrained by `type`: when type is 'device' this must be exactly 'any', 'mobile', or 'desktop'.
          - `filter` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', nullable: Comparison operator (e.g., equals, contains, startsWith).
          - `type` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage', required: Category of the parameter being matched. Determines which keys are valid. Use 'device' for device targeting (pair with key 'device', filter 'equals', and value 'any' | 'mobile' | 'desktop'); 'utm'/'url' for query params; 'urlPath' for the path; and 'country', 'visitor', 'trafficSource', 'referrer', 'cookie', 'landingPage', 'klaviyo', or 'jsExpression' for the rest.
        - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
      - `experienceId` string, uuid
    - `experienceProductTargeting` object[], required: Product attribute rules controlling which products this experience applies to. Empty means all products.
      - `id` string, uuid, required: Server-generated. Read-only.
      - `experienceId` string, uuid, required: References the parent experience.
      - `target` 'productPage' | 'productCard', required: Storefront context where targeting rules are evaluated (product detail pages vs collection/search listings).
      - `expression` object[], nullable: Boolean expression tree of product attribute conditions. Each node has either a 'query' (leaf) or 'operator' (combinator).
        - `query` object
          - `key` 'productId' | 'collection' | 'tag' | 'price' | 'inventory', required: Product attribute to filter on. Determines which product property is evaluated.
          - `value` union, required: Expected value. String for text attributes (productId, collection, tag); number for numeric attributes (price, inventory).
            - string
            - number
          - `filter` 'includes' | 'doesNotInclude' | 'greaterThan' | 'lessThan', required: Comparison operator for evaluating the product attribute against the value.
          - `type` 'productId' | 'collection' | 'tag' | 'price' | 'inventory', required: Mirrors the key field. Determines attribute category for validation.
        - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
    - `experienceActions` object[], required: Server-generated. Read-only. Lifecycle state transitions (start, pause, end, delete, reset) recorded for this experience. On GET /experiences/{experienceId}, each entry includes a `performedBy` field with the triggering user's email, resolved from History records (may be null if unresolvable). Distinct from the list endpoint's raw `history` field, which contains broader user-action records.
      - `id` string, uuid, required
      - `experienceId` string, uuid, required
      - `action` 'start' | 'pause' | 'end' | 'delete' | 'reset', required
      - `actionTs` number, required
      - `performedBy` string, nullable: Email of the user who triggered this lifecycle event. Resolved from History records by matching action type and timestamp within a 5-minute window. Null if no matching History record is found, or if the record has no associated user. Currently only populated by GET /experiences/{experienceId}; list endpoints return null.
    - `experienceIntegrations` object[], required: Connected third-party integrations (e.g., Google Analytics, Segment).
      - `id` string, uuid, required
      - `createdAtTs` number, required
      - `integration` object, required
        - `id` string, uuid, required
        - `name` 'Recharge' | 'StayAi' | 'GrowLTV' | 'OneClickUpsell' | 'Google' | 'Amped' | 'Clarity' | 'Hotjar' | 'Heatmap' | 'CustomApi' | 'Klaviyo' | 'Slack' | 'Amplitude' | 'Heap' | 'Segment' | 'IntelligemsAPI' | 'IntelligemsMcp' | 'UPS' | 'MetaAds', required
        - `description` string, required
        - `enabled` boolean, required
        - `globalSettings` unknown, required
        - `apis` object[], required
          - `id` string, uuid, required
          - `name` string, required
          - `description` string, nullable
          - `accessToken` string, required
          - `scopes` string[], required
          - `organization` object, required
            - `id` string, uuid, required
            - `name` string, required
        - `createdAtTs` number
        - `lastUpdateTs` number
      - `experienceSettings` unknown, required
      - `enabled` boolean, required
    - `experienceCustomMetrics` object[], required: Custom event-based metrics tracked alongside standard key metrics.
      - `id` string, uuid, required: Server-generated. Read-only.
      - `experienceId` string, uuid, required: References the parent experience.
      - `order` number, required: Display position in the analytics dashboard. 0-indexed.
      - `customEventId` string, uuid, required: References the custom event definition configured in the organization's analytics settings.
      - `customEvent` union, required: Server-generated. Read-only. Expanded custom event definition with name, tracking details, and configuration.
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'pageView', required
          - `settings` object, required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'productPageView', required
          - `settings` object[], required
            - `productId` string, required
            - `handle` string, required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'collectionPageView', required
          - `settings` object[], required
            - `handle` string, required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'clickEvent', required
          - `settings` object, required
            - `selectors` string[], required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'elementViewed', required
          - `settings` object, required
            - `selectors` string[], required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'scrollDepth', required
          - `settings` object, required
            - `mobile` object, required
              - …
            - `desktop` object, required
              - …
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'javascriptEvent', required
          - `settings` object, required
            - `code` string, required
        - object
          - `id` string, uuid, required
          - `identifier` string, required
          - `name` string, required
          - `description` string, nullable
          - `eventCountLastDay` number, nullable
          - `lastEventTs` number, nullable
          - `error` string, nullable
          - `createdAtTs` number, required
          - `lastUpdateTs` number, required
          - `archivedAtTs` number, nullable
          - `applyToUrls` object[], nullable
            - `matchBy` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', required
            - `value` string, required
            - `error` string, nullable
          - `experiencesList` string[], nullable
          - `type` 'javascriptImportedEvent', required
          - `settings` object, required
            - `registered` boolean, required
    - `experienceKeyMetrics` object[], required: Business metrics (revenue, conversion, AOV, etc.) used to evaluate test performance. One must be primary.
      - `id` string, uuid, required: Server-generated. Read-only.
      - `experienceId` string, uuid, required: References the parent experience.
      - `order` integer, required: Display position in the analytics dashboard. 0-indexed.
      - `isPrimary` boolean, required: The primary metric determines the main success signal for the test. Exactly one per experience.
      - `standardEventId` 'conversionRate' | 'addToCartRate' | 'abandonedCartRate' | 'abandonedCheckoutRate' | 'checkoutBeginRate' | 'checkoutEnterContactInfoRate' | 'checkoutAddressSubmittedRate' | 'viewProductPageRate' | 'viewCollectionPageRate' | 'netRevenuePerVisitor' | 'netProductRevenuePerOrder' | 'netRevenuePerOrder' | 'avgUnitsPerOrder' | 'avgProductRevenuePerUnit' | 'profitPerVisitor' | 'profitPerOrder' | 'pctOrdersFreeShipping' | 'netShippingRevenuePerOrder' | 'subscriptionOrdersPerVisitor' | 'subscriptionRevenuePerVisitor' | 'subscriptionProfitPerVisitor' | 'subscriptionProductRevenuePerOrder' | 'avgDiscountPerDiscountedOrder' | 'avgDiscountPerAllOrders' | 'pctSubscriptionOrders' | 'subscriptionRevenuePerOrder' | 'pctOrdersWithPostPurchaseOffer' | 'netRevenueFromPostPurchaseOffer' | 'pctOrdersWithUpsell' | 'netRevenueFromUpsell' | 'pctOrdersWithOnsiteUpsell' | 'netRevenueFromOnsiteUpsell', nullable: Standard e-commerce event (e.g., revenue, conversion). Mutually exclusive with experienceCustomMetricId.
      - `experienceCustomMetricId` string, uuid, nullable: References a custom event metric. Mutually exclusive with standardEventId. Exactly one of these must be set.
    - `currency` object, nullable: Currency settings for price display and analytics calculations. Null inherits store default.
      - `code` string, required: ISO 4217 currency code used for analytics and price calculations.
      - `country` string: ISO country code for locale-specific formatting.
      - `options` unknown
      - `symbol` string, required: Display symbol prepended to amounts (e.g., '$', '€').
      - `suffix` string: Text appended after amounts for postfix currencies (e.g., 'kr' in '100 kr').
    - `variations` object[], required: Test variants including the control. One must be control; traffic percentages must sum to 100.
      - `id` string, uuid, required: Server-generated. Read-only.
      - `experienceId` string, uuid, required: References the parent experience.
      - `name` string, required: Display name (e.g., 'Control', 'Variant A'). Unique within the experience.
      - `percentage` number, required: Traffic allocation weight. All percentages across sibling variations must sum to 100.
      - `isControl` boolean, required: The baseline variation all others are measured against. Exactly one per experience.
      - `order` integer, required: Display order in the analytics dashboard. 0-indexed.
      - `priceChange` string, nullable: Price adjustment amount for pricing-type experiences. Positive increases price, negative discounts. Use priceChange for simple price A/B tests; use offer for structured discount campaigns (tiered, BOGO, free shipping).
      - `priceChangeUnit` 'percent' | 'dollar', nullable: How priceChange is applied: as a percentage of the original price or a fixed monetary amount.
      - `priceRoundingType` 'none' | 'nearest' | 'up' | 'down', nullable: Rounding strategy applied after priceChange (e.g., round up, round down, charm pricing).
      - `priceRoundingAmount` string, nullable: Target value for rounding (e.g., 0.99 for charm pricing like $X.99).
      - `onsiteEdits` object[], required: DOM content modifications (text, HTML, image swaps, element removal). For CSS styling changes, use onsiteInjections instead.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `onsiteEditType` 'html' | 'text' | 'image' | 'hide', nullable: Content modification strategy (html, text, image, or hide).
        - `variationId` string, uuid, required: References the parent variation.
        - `find` string, nullable, required: Pattern to locate on the page. Interpreted as regex when isRegex is true.
        - `title` string, nullable: Dashboard label for identifying this edit.
        - `isRegex` boolean, required: When true, 'find' is treated as a regular expression.
        - `replace` string, required: Replacement content. Interpretation depends on onsiteEditType: HTML markup, plain text, or image URL.
        - `querySelectors` string[], required: CSS selectors scoping which DOM elements this edit targets (e.g., '.product-title').
        - `skip` boolean, required: When true, this edit is temporarily disabled without being deleted.
        - `remove` boolean, required: When true, the matched element is removed from the DOM instead of having its content replaced.
        - `groupId` string: Groups related edits together so they can be managed as a unit.
        - `isHtml` boolean, required
        - `isImage` boolean, required
        - `insertType` 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', nullable, required: DOM insertion position relative to the target (beforebegin, afterbegin, beforeend, afterend). Null means content replacement.
        - `createdAtUrl` string, nullable: Server-generated. Read-only. Storefront URL where this edit was originally created via the visual editor.
        - `createdAtTs` number, nullable: Server-generated. Read-only. Unix timestamp in seconds.
        - `imageSearchUrl` string, nullable: URL pathname for locating images to replace. When set, images are found by URL match; when null, querySelectors is used instead.
        - `source` 'ai', nullable
      - `onsiteInjections` object, nullable: CSS styling and custom JS injection. At most one per variation. For DOM content changes (text, images, HTML), use onsiteEdits instead.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `variationId` string, uuid, required: References the parent variation.
        - `customCss` string, nullable: CSS injected globally on the page when this variation is active.
        - `customJs` string, nullable: JavaScript executed on the page. Execution timing controlled by jsInjectionMode.
        - `jsInjectionMode` object, required: Controls when customJs runs: immediately on load, after window load, or after a configurable delay.
          - `type` 'immediately' | 'onWindowLoad' | 'timeout', required: Execution timing for injected JS. When 'timeout', the timeout field specifies the delay.
          - `timeout` union: Delay in milliseconds before JS execution. Only relevant when type is 'timeout'.
            - number
            - string
        - `source` 'ai', nullable
      - `offer` object, nullable: Structured discount configuration (tiered volume discounts, cart-level discounts, gift-with-purchase, free shipping). Use offer for campaign/discount experiences; use priceChange for simple price A/B tests.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `variationId` string, uuid, required: References the parent variation this offer belongs to.
        - `enabled` boolean, required: Whether the offer is actively applying discounts to customers on the storefront.
        - `isTest` boolean, required: Set on some offers the app copied between test groups. It doesn't mean draft and doesn't change who gets the discount; an offer saved with it true is renamed 'Volume Offer Test Group'. The external API always saves false.
        - `isArchived` boolean, required: Whether the offer has been archived and is no longer active.
        - `name` string, required: Internal label for identifying the offer in dashboards.
        - `unitType` 'unit' | 'dollar', required: Threshold basis for tiered discounts: per-item quantity or monetary amount spent.
        - `discountType` 'percentage' | 'dollar', required: How the discount value is calculated (e.g., percentage off, fixed amount off).
        - `tiers` object[], required: Volume discount tiers ordered by threshold ascending (e.g., buy 2 = 10% off, buy 3 = 15% off).
          - `id` string, uuid, required
          - `minimumUnits` number, required
          - `maximumUnits` number, required
          - `unitDiscount` number, required
          - `bxgyGetQuantity` integer, required
          - `discountTitle` string, nullable, required
          - `giftWithPurchaseTitle` string, nullable, required
          - `isFreeShipping` boolean, required
          - `isGiftWithPurchase` boolean, required
          - `giftWithPurchaseProductId` string, nullable
          - `giftWithPurchaseVariantId` string, nullable
          - `giftWithPurchaseVariantIds` string[], required
          - `autoAddGiftWithPurchase` boolean, required
          - `giftWithPurchaseHandle` string, nullable
        - `maximumDollarDiscount` number, nullable: Hard cap on the total dollar discount. Null means no cap is applied.
        - `discountApplicationType` 'tieredDiscount' | 'cartDiscount' | 'freeGift' | 'freeShipping' | 'buyXGetY' | '', required: Core discount mechanic (tiered volume, flat cart-level, gift-with-purchase, or free shipping).
        - `discountApplicationMethod` 'perItem' | 'perOrder', required: Whether the discount applies per individual item or to the entire order.
        - `combinesWithIntelligems` boolean, required: Whether this offer stacks with other active Intelligems discounts.
        - `combinesWithShopify` boolean, required: Whether this offer stacks with native Shopify discount codes.
        - `shouldRejectCodes` boolean, required: When true, manual discount codes entered at checkout are rejected while this offer is active.
        - `shippingDiscount` object, nullable, required: Shipping discount details. Only present when discountApplicationType is freeShipping.
          - `id` string, uuid, required
          - `discountType` 'percentage' | 'dollar', required
          - `unitDiscount` number, required: Percent off the shipping rate. 100 is free shipping.
          - `rateFilter` union, required
            - object
              - …
            - object
              - …
          - `countryFilter` object, nullable, required
            - `allow` string[]
            - `exclude` string[]
          - `discountLabel` string, nullable, required
        - `requireSameItem` boolean, required: For tiered discounts: when true, only multiples of the same product count toward thresholds.
        - `bxgyGetProductScope` 'all' | 'list', nullable: For Buy X Get Y: where the free units come from. Null means the products bought; 'list' means the offer's get products; 'all' means any product.
        - `subscriptionApplicationType` 'oneTime' | 'firstSub' | 'oneTimeAndFirstSub', required: Controls which purchase types this offer applies to: 'oneTime' (one-time purchases only), 'firstSub' (first subscription orders only), or 'oneTimeAndFirstSub' (both one-time and first subscription orders). Free shipping offers (discountApplicationType 'freeShipping') always apply to both and are saved as 'oneTimeAndFirstSub'.
      - `offerId` string, uuid, nullable: Server-generated. Read-only. References the associated offer. Populated when offer is set.
      - `shippingRateGroups` object[]: Shipping rate overrides. Only populated for shipping test experiences.
        - `id` string, uuid, required
        - `groupType` 'flatRate' | 'flatRateWithThreshold' | 'thresholdOnly' | 'tieredByPriceOrWeight' | 'custom', required
        - `position` number, required
        - `rates` object[], required
          - `id` string, uuid
          - `name` string, required
          - `amount` number, required
          - `rateCode` string, nullable
          - `rateType` 'amount' | 'threshold' | 'tier', required
          - `currency` 'AED' | 'AFN' | 'ALL' | 'AMD' | 'ANG' | 'AOA' | 'ARS' | 'AUD' | 'AWG' | 'AZN' | 'BAM' | 'BBD' | 'BDT' | 'BGN' | 'BIF' | 'BMD' | 'BND' | 'BOB' | 'BRL' | 'BSD' | 'BWP' | 'BZD' | 'CAD' | 'CDF' | 'CHF' | 'CLP' | 'CNY' | 'COP' | 'CRC' | 'CVE' | 'CZK' | 'DJF' | 'DKK' | 'DOP' | 'DZD' | 'EGP' | 'ETB' | 'EUR' | 'FJD' | 'FKP' | 'GBP' | 'GEL' | 'GIP' | 'GMD' | 'GNF' | 'GTQ' | 'GYD' | 'HKD' | 'HNL' | 'HRK' | 'HTG' | 'HUF' | 'IDR' | 'ILS' | 'INR' | 'ISK' | 'JMD' | 'JPY' | 'KES' | 'KGS' | 'KHR' | 'KMF' | 'KRW' | 'KYD' | 'KZT' | 'LAK' | 'LBP' | 'LKR' | 'LRD' | 'LSL' | 'MAD' | 'MDL' | 'MGA' | 'MKD' | 'MMK' | 'MNT' | 'MOP' | 'MUR' | 'MVR' | 'MWK' | 'MXN' | 'MYR' | 'MZN' | 'NAD' | 'NGN' | 'NIO' | 'NOK' | 'NPR' | 'NZD' | 'PAB' | 'PEN' | 'PGK' | 'PHP' | 'PKR' | 'PLN' | 'PYG' | 'QAR' | 'RON' | 'RSD' | 'RUB' | 'RWF' | 'SAR' | 'SBD' | 'SCR' | 'SEK' | 'SGD' | 'SHP' | 'SLL' | 'SRD' | 'STD' | 'SZL' | 'THB' | 'TJS' | 'TOP' | 'TRY' | 'TTD' | 'TWD' | 'TZS' | 'UAH' | 'UGX' | 'USD' | 'UYU' | 'UZS' | 'VND' | 'VUV' | 'WST' | 'XAF' | 'XCD' | 'XOF' | 'XPF' | 'YER' | 'ZAR' | 'ZMW', required
          - `condition` object, nullable
            - `id` string, uuid
            - `min` union, required
              - …
            - `max` union
              - …
            - `conditionType` 'currency' | 'weight', required
            - `unit` 'g' | 'kg' | 'lb' | 'oz', nullable
          - `description` string, nullable
        - `variationId` string, uuid, required
        - `scopes` object[], nullable
          - `marketId` string, required
          - `collectionIds` string[], nullable, required
          - `originLocationIds` string[], required
      - `excludeProducts` boolean: When true, specific products are excluded from price changes. Use excludeProducts when most products should be tested and only a few excluded. Mutually exclusive with includeProducts.
      - `includeProducts` boolean: When true, only specific products receive price changes. Use includeProducts when only a few products should be tested. Mutually exclusive with excludeProducts.
      - `testExperienceId` string, nullable: Links to another experience for advanced multi-experience configurations.
      - `functionSyncError` boolean, required: Server-generated. Read-only. Indicates a sync failure with Shopify Functions for this variation's discount.
      - `redirects` object[]: URL redirect rules for content/url experiences.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `group` number, required: Numeric group for organizing related redirects. Redirects sharing an originUrl should share a group number.
        - `variationId` string, uuid, required: References the parent variation.
        - `originUrl` string, required: URL pattern matched against visitor page URLs. Matching behavior depends on the 'filter' field.
        - `destinationUrl` string, nullable: Target URL visitors are sent to. Used when redirectType is 'fullReplacement'.
        - `queryParams` object[], required: Key-value pairs appended to the destination URL as query parameters.
          - `key` string, required: Query parameter name appended to the destination URL.
          - `value` string, required: Query parameter value.
        - `skip` boolean, required: When true, this redirect is temporarily disabled.
        - `isThemeTest` boolean, required: True when this redirect tests alternate Shopify themes. Used with content/theme experiences.
        - `isTemplateTest` boolean, required: True when this redirect tests alternate Shopify templates. Used with content/template experiences.
        - `templateType` '404' | '' | 'article' | 'blog' | 'cart' | 'collection' | 'index' | 'list-collections' | 'page' | 'password' | 'product' | 'search', nullable: Shopify template type being tested (e.g., product, collection).
        - `templateSuffixes` string[], nullable, required: Alternate Shopify template suffixes being tested (e.g., 'alternate', 'new-layout').
        - `redirectOnce` boolean, required: When true, the redirect fires only once per visitor session to avoid redirect loops.
        - `filter` 'matchesExactly' | 'contains' | 'endsWith' | 'matchesRegex' | 'none', required: Matching strategy for originUrl (e.g., exact match, contains, endsWith, regex).
        - `find` string, nullable: Pattern to locate within the URL for partial replacement. Only used when redirectType is 'partialReplacement'.
        - `redirectType` 'fullReplacement' | 'partialReplacement', required: fullReplacement navigates to destinationUrl; partialReplacement swaps only the 'find' match within the current URL.
      - `userInterfaces` object[]: UI widget configurations displayed for this variation.
        - `id` string, uuid, required
        - `isEnabled` boolean
        - `isArchived` boolean, required
        - `widget` object
          - `id` string, uuid, required
          - `name` string, required
          - `enabledSitewide` boolean, required
          - `parentId` string, uuid, nullable
          - `widgetType` 'messageBoxPopup' | 'messageBoxSlideOut' | 'quantityButtons' | 'discountProgressBar' | 'shippingProgressBar' | 'gwpVariantPicker', required
          - `config` object, required
            - `desktop` unknown
            - `mobile` unknown
        - `widgetId` string, required
        - `category` 'quantityButtons' | 'progressBar' | 'messageBox' | 'gwpVariantPicker'
        - `variationId` string, uuid, nullable
        - `desktopVariables` unknown
        - `mobileVariables` unknown
        - `styleOverrides` unknown
      - `checkoutBlocks` union[]: Shopify checkout extensibility block configurations for this variation.
        - union
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'trustBadge', required
            - `blockTemplate` 'badgeList' | 'badgeTiles' | 'callOut' | 'grid' | 'header' | 'images' | 'brandLogo' | 'textOnly', nullable: The template preset this block was created from, within its block type. Null on blocks saved before templates were persisted, which callers must treat as unknown rather than assuming a default.
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required: Configurable content for the trust badge (e.g., badge images, guarantee text).
              - …
            - `styles` object, required: CSS styling overrides for the trust badge appearance.
              - …
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'upsell', required
            - `blockTemplate` 'productTiles' | 'productList' | 'singleProduct' | 'singleProductCheckbox', nullable: The template preset this block was created from, within its block type. Null on blocks saved before templates were persisted, which callers must treat as unknown rather than assuming a default.
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required: Configurable content for the upsell block (e.g., product selection, heading text, CTA label).
              - …
            - `styles` object, required: CSS styling overrides for the upsell block appearance.
              - …
            - `upsellProductRules` object[], required: Product selection and recommendation rules that determine which products to show in this upsell block.
              - …
            - `agenticLogicPlacementId` string, uuid, nullable: Agentic Logic placement this block evaluates through. Absent means the legacy rule engine serves it — clearing the mapping is the per-surface rollback.
            - `agenticLogicRuleId` string, uuid, nullable: WRITE-SIDE intent: the Agentic Logic rule this block should serve. The experience save upserts the placement and the (placement, variation) assignment from it and stamps `agenticLogicPlacementId`, so callers never assemble that pair themselves. Null detaches the block and returns it to the legacy engine. Reads echo the rule currently assigned.
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'shippingProgressBar', required
            - `blockTemplate` 'minimal' | 'milestones', nullable: The template preset this block was created from, within its block type. Null on blocks saved before templates were persisted, which callers must treat as unknown rather than assuming a default.
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required
              - …
            - `styles` object, required
              - …
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'timer', required
            - `blockTemplate` 'textCountdown' | 'digitalClock' | 'inlinePill', nullable: The template preset this block was created from, within its block type. Null on blocks saved before templates were persisted, which callers must treat as unknown rather than assuming a default.
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required
              - …
            - `styles` object, required
              - …
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'faqs', required
            - `blockTemplate` 'faqsAccordion', nullable: The template preset this block was created from, within its block type. Null on blocks saved before templates were persisted, which callers must treat as unknown rather than assuming a default.
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required
              - …
            - `styles` object, required
              - …
          - object
            - `id` string, uuid, required: Server-generated. Read-only.
            - `variationId` string, uuid, required: References the parent variation.
            - `locationId` string, required: Shopify checkout extensibility target location where the block renders.
            - `blockType` 'postPurchaseUpsell', required
            - `blockTemplate` unknown
            - `name` string, required: Display name for identifying this block in the dashboard.
            - `displayConditions` object: Optional cart-based conditions; the block renders only when the expression evaluates true against the current cart.
              - …
            - `variables` object, required
              - …
            - `styles` object, required
              - …
            - `upsellProductRules` object[], required: Product selection and recommendation rules that determine which products to show in this upsell block.
              - …
            - `agenticLogicPlacementId` string, uuid, nullable: Agentic Logic placement this block evaluates through. Absent means the legacy rule engine serves it — clearing the mapping is the per-surface rollback.
            - `agenticLogicRuleId` string, uuid, nullable: WRITE-SIDE intent: the Agentic Logic rule this block should serve. The experience save upserts the placement and the (placement, variation) assignment from it and stamps `agenticLogicPlacementId`, so callers never assemble that pair themselves. Null detaches the block and returns it to the legacy engine. Reads echo the rule currently assigned.
      - `cartTargeting` object[]: Cart-based targeting rules that determine when this variation is shown based on cart contents.
        - `id` string, uuid, required
        - `expression` object[], required
          - `query` object
            - `key` 'anything' | 'productId' | 'collection' | 'tag' | 'cartSubtotal' | 'cartItemCount' | 'subscription', required
            - `type` 'anything' | 'productId' | 'collection' | 'tag' | 'cartSubtotal' | 'cartItemCount' | 'subscription', required
            - `filter` 'includes' | 'doesNotInclude' | 'greaterThan' | 'lessThan' | 'equalTo' | 'greaterThanOrEqual' | 'lessThanOrEqual' | 'between', required
            - `value` union
              - …
          - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
      - `components` object[]: Onsite component configurations (e.g. onsite upsells) rendered on the storefront for this variation.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `variationId` string, uuid, required: References the parent variation this component belongs to.
        - `config` union, required: The saved upsell: its type, look, products, targeting, and discount.
          - object
            - `type` 'onsiteUpsell', required: Discriminator for the component kind.
            - `placement` object, required: Where in the DOM the rendered upsell is injected.
              - …
            - `template` string, required: HTML for the upsell, including all static copy written inline. The per-product card is the markup between {{#each products}} and {{/each}}. May only reference declared slots: product.* data slots and the named slots (atc, variant-select); everything else is literal HTML.
            - `maxProductLimit` integer, required: Maximum number of products to render.
            - `excludeOutOfStock` boolean, required: Exclude products whose variants are all out of stock. Defaults to true.
            - `excludeItemsInCart` boolean, required: Exclude products already in the shopper's cart. Defaults to true.
            - `excludePdpProduct` boolean, required: On a PDP, exclude the product being viewed. Defaults to true; turn it off when the viewed product belongs in the result, e.g. a bundle built around it.
            - `discount` object: Optional discount applied to products added from this upsell.
              - …
            - `rules` object[], required: Prioritized list of rules. Each pairs an action (which products) with targeting (when to show). v1 ships a single rule.
              - …
            - `agenticLogicPlacementId` string, uuid, nullable: Agentic Logic placement this upsell evaluates through. Absent means the legacy rule engine serves it — clearing the mapping is the per-surface rollback.
            - `agenticLogicRuleId` string, uuid, nullable: WRITE-SIDE intent: the Agentic Logic rule this upsell should serve. The experience save upserts the placement and the (placement, variation) assignment from it and stamps `agenticLogicPlacementId`, so callers never assemble that pair themselves. Null detaches the component and returns it to the legacy engine. Reads echo the rule currently assigned; the field is never persisted in the stored config.
          - object
            - `type` 'bundleOnsiteUpsell', required: Discriminator for the component kind.
            - `placement` object, required: Where in the DOM the rendered bundle is injected.
              - …
            - `template` string, required: HTML for the bundle card. The per-product row is the markup between {{#each products}} and {{/each}}; the surrounding shell holds the copy slots ({{heading}}, {{subheading}}) and the footer the whole selection shares ({{bundle.price}}, {{bundle.comparePrice}}, {{bundle.atc}}). Each row holds {{checkbox}}, {{product.image}}, {{product.url}}, {{product.title}}, {{product.price}}, {{product.compareAtPrice}} and {{variant-select}}. Every slot is required, even when editing. May only reference declared slots; everything else is literal HTML. No `<script>` tags or inline JavaScript.
            - `copy` object, required: Shopper-facing text, edited in the panel rather than inline in the template.
              - …
            - `maxProductLimit` integer, required: Number of products the bundle offers.
            - `allowItemSelection` boolean, required: Whether the shopper can choose which of the bundle's products to add. When false every rendered product is included; exclusions can still trim how many render, so lower tiers stay reachable.
            - `excludeOutOfStock` boolean, required: Exclude products whose variants are all out of stock. Defaults to true.
            - `excludeItemsInCart` boolean, required: Exclude products already in the shopper's cart. Defaults to true.
            - `excludePdpProduct` boolean, required: On a PDP, exclude the product being viewed. Defaults to true; turn it off when the viewed product belongs in the result, e.g. a bundle built around it.
            - `discount` object: Optional count-tiered discount applied to the products added from this bundle.
              - …
            - `rules` object[], required: Prioritized list of rules. Each pairs an action (which products) with targeting (when to show).
              - …
            - `agenticLogicPlacementId` string, uuid, nullable: Agentic Logic placement this bundle selects its products through. Absent means the legacy rule engine serves it — clearing the mapping is the per-surface rollback. The engine chooses PRODUCTS only: a bundle's tiered discount always comes from `discount` below, never from the result envelope, which can only carry one flat value.
            - `agenticLogicRuleId` string, uuid, nullable: WRITE-SIDE intent: the Agentic Logic rule this bundle should serve. The experience save upserts the placement and the (placement, variation) assignment from it and stamps `agenticLogicPlacementId`, so callers never assemble that pair themselves. Null detaches the component and returns it to the legacy engine. Reads echo the rule currently assigned; the field is never persisted in the stored config. The engine chooses PRODUCTS only — a bundle's tiers always come from its own `discount`.
    - `createdAtTs` number, required: Server-generated. Read-only. Unix timestamp in milliseconds.
    - `isDeleted` boolean: Whether this experience has been soft-deleted and hidden from default listings.
    - `shippingTestMethodDefinitions` union[], required: Shipping rate configurations. Only populated for shipping test experiences.
      - union
        - object
          - `id` string, uuid
          - `shippingModel` 'legacy'
          - `deliveryProfileId` string, required
          - `deliveryLocationGroupId` string, required
          - `deliveryZoneId` string, required
          - `methodDefinitionId` string, nullable
          - `canonicalMethodId` string
          - `deleteTargetKind` 'canonical_method' | 'participant_method' | 'synthetic_free_condition' | 'synthetic_rate_definition'
          - `deleteTargetRateDefinitionId` string
          - `deleteTargetFreeCondition` object
            - `subject` string, required
            - `min` union
              - …
            - `max` union
              - …
        - object
          - `id` string, uuid
          - `shippingModel` 'markets', required
          - `marketTarget` union, required
            - object
              - …
            - object
              - …
            - object
              - …
            - object
              - …
    - `shippingCarrierProvider` 'intelligems' | 'custom', required: How shipping rates are sourced for this shipping test. Use "intelligems" for the managed carrier flow and "custom" to use custom provider settings.
    - `audience` object, nullable: Visitor segmentation rules. Null means all visitors are eligible.
      - `id` string, uuid, required: Server-generated. Read-only.
      - `experienceId` string, uuid, required: References the parent experience.
      - `enabled` boolean, required: When false, audience rules are ignored and all visitors are eligible.
      - `excludeCurrency` object, required: Currency and geo-based visitor exclusion configuration.
        - `exclude` boolean, required: Whether currency/geo-based exclusion is active.
        - `currency` string, nullable, required: ISO 4217 currency code to include. Null disables currency-based exclusion.
        - `country` string: ISO country code to include visitors from.
        - `region` string: Region or state code to include visitors from.
        - `city` string: City name to include visitors from.
      - `wholesale` boolean, required: Controls whether wholesale (B2B) customers are included in or excluded from the experience.
      - `audienceType` 'common' | 'custom' | 'advanced': Complexity level of the targeting rules: common (prebuilt), custom (simple), or advanced (full expression trees).
      - `elseAction` 'experienceInclude' | 'experienceExclude' | 'experienceUnassigned' | 'assignVariation' | 'randomVariation' | 'leaveUnassigned', nullable, required: Fallback for visitors matching no filter: 'randomVariation' assigns randomly across variations (most common), 'assignVariation' forces a specific variation (set elseVariationId), 'experienceExclude' removes them from the experience entirely, 'experienceUnassigned' leaves them unassigned for now ('leaveUnassigned' is the older spelling and is stored as experienceUnassigned).
      - `elseVariationId` string, uuid, nullable: Variation to force-assign when elseAction is 'assignVariation'. Typically set to the control variation to default non-targeted visitors to baseline.
      - `elseExcludeFromAnalytics` boolean, required: When true, visitors who match no filter are tracked but excluded from analytics calculations.
      - `filters` object[], required: Ordered filter rules evaluated from lowest priority value first (0-indexed). First matching filter determines visitor treatment.
        - `id` string, uuid, required: Server-generated. Read-only.
        - `priority` integer, required: Evaluation order. 0-indexed. Filters with lower values are evaluated first.
        - `action` 'experienceInclude' | 'experienceExclude' | 'experienceUnassigned' | 'assignVariation' | 'randomVariation' | 'leaveUnassigned', nullable, required: What happens when a visitor matches this filter: 'assignVariation' forces a specific variation (set variationId), 'experienceExclude' removes from the experience, 'randomVariation' assigns randomly. 'experienceInclude' shows a personalization to the visitor and is valid only on personalizations, whose filters use experienceInclude, experienceExclude or experienceUnassigned and never a traffic split.
        - `filterType` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage', required: Category of targeting rule (e.g., UTM parameters, device type, visitor type).
        - `expression` object[], nullable: Boolean expression tree. Each node has either a 'query' (leaf condition) or 'operator' (logical combinator), not both.
          - `query` object
            - `key` string, required: Visitor attribute or parameter name to match (e.g., 'utm_source', 'device_type').
            - `value` string, required: Expected value to compare against the visitor's actual value for this key. Constrained by `type`: when type is 'device' this must be exactly 'any', 'mobile', or 'desktop'.
            - `filter` 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'doesNotEqual' | 'doesNotContain' | 'doesNotStartWith' | 'doesNotEndWith' | 'isNull' | 'isNotNull' | 'regex', nullable: Comparison operator (e.g., equals, contains, startsWith).
            - `type` 'utm' | 'url' | 'urlPath' | 'device' | 'visitor' | 'purchaseLikelihood' | 'jsExpression' | 'trafficSource' | 'country' | 'referrer' | 'cookie' | 'klaviyo' | 'landingPage', required: Category of the parameter being matched. Determines which keys are valid. Use 'device' for device targeting (pair with key 'device', filter 'equals', and value 'any' | 'mobile' | 'desktop'); 'utm'/'url' for query params; 'urlPath' for the path; and 'country', 'visitor', 'trafficSource', 'referrer', 'cookie', 'landingPage', 'klaviyo', or 'jsExpression' for the rest.
          - `operator` 'and' | 'or' | 'start_paren' | 'end_paren'
        - `expressionType` 'common' | 'custom', nullable: Whether this uses prebuilt common rules or custom expression logic.
        - `variationId` string, uuid, nullable: Variation to assign when action is 'assignVariation'. References a variation in the parent experience.
        - `excludeFromAnalytics` boolean, required: When true, visitors matching this filter are excluded from analytics even if included in the experience.
      - `evaluationFrequency` number, nullable: How often (in seconds) to re-evaluate audience rules for a returning visitor. Controls targeting freshness.
    - `lastUpdateTs` number, required: Server-generated. Read-only. Unix timestamp in milliseconds of the most recent modification.
    - `previewPath` string, nullable: Storefront URL path used when generating preview links (e.g., '/products/example').
    - `isExternal` boolean, nullable: True if this experience was created and is managed via the external API rather than the dashboard.
    - `startedAtTs` number, nullable, required: Server-generated. Read-only. Unix timestamp in milliseconds. Null if never started.
    - `pausedAtTs` number, nullable, required: Server-generated. Read-only. Unix timestamp in milliseconds. Null if not currently paused.
    - `archivedAtTs` number, nullable: Server-generated. Read-only. Unix timestamp in milliseconds. Null if not archived.
    - `requiresLink` boolean, required: When true, visitors must use a special link to enter the experience instead of being auto-assigned.
    - `linkBaseUrl` string, nullable, required: Base URL for generating access links. Only populated when requiresLink is true.
    - `endedAtTs` number, nullable, required: Server-generated. Read-only. Unix timestamp in milliseconds. Null if still active.
    - `productVariantCount` number, required: Server-generated. Read-only. Total product variants currently included in this experience.
    - `sourceExperienceId` string, uuid, nullable: Present when this experience was duplicated or rolled out from another. References the original.
    - `sourceAction` 'duplicate' | 'rollout', nullable: How this experience was derived from sourceExperienceId. Only present when sourceExperienceId is set.
    - `experienceAnalysis` object, required
      - `analyticsViewType` 'test_orders_only' | 'sitewide_orders' | 'none', required: Scope of orders included in analytics results. Determines whether only tested products or all store orders are counted.

## Other responses

- `429`: Too Many Requests

## Changes

- **2026-11-06** `7b769b2f90b1`: 66 breaking, 29 warning, 107 info
  - added the new required request property `experience/variations/items/offer/anyOf[subschema #1]/tiers/items/giftWithPurchaseVariantIds`
  - the request property `experience/currency/anyOf[subschema #1]/code` became required
  - the `discountType` request property const value `percentage` was added
  - removed the enum value `` of the request property `experience/variations/items/offer/anyOf[subschema #1]/discountApplicationType`
  - …198 more

[Operation history](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/changes/v25-10-beta/experiences/:experienceId/put.md)

---

[API](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta.md) - [All operations](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/llms.txt) - [OpenAPI document](https://skmtc.dev/intelligems/apis/intelligems-external-api-v25-10-beta/revisions/7b769b2f90b1?raw)
