> ## Documentation Index
> Fetch the complete documentation index at: https://docs.abconvert.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a test

> Creates a test in `draft`. Launching is a separate step: call
[`POST /experiments/{id}/start`](/api-reference/lifecycle/launch-a-test).

You can include `schedule` in the same call, so a single request
creates a scheduled draft.

What you can't create here: a visual editor test's DOM changes,
`combined` tests, checkout block customizations, and offer widgets.
You author those in the ABConvert admin, and they read back read-only.
A `visual_editor` test itself is creatable, carrying custom CSS and
JavaScript per test group.

For the plan each test type needs, see
[Feature availability](/api-reference/overview#feature-availability).




## OpenAPI

````yaml /api-reference/openapi.yaml post /experiments
openapi: 3.1.0
info:
  title: ABConvert Public API
  version: 1.0.0-beta
  description: |
    Create, read, update, and run A/B tests on your Shopify store from your
    own code.

    This version covers test CRUD, lifecycle actions, archiving, results,
    order exports, scheduling, and token scopes. There are no webhooks: to
    see a test's current state, call
    [`GET /experiments/{id}`](/api-reference/experiments/retrieve-a-test).

    Every path, field, and identifier in this API spells the resource
    `experiment`. An experiment is a test: the same object you create and
    run in the ABConvert admin. These docs say "test" in prose.

    A finding is one problem the server found, carrying a `code`, a
    `message`, and a `severity`. An `error` finding blocks the request and
    returns 422. A `warning` finding rides along with a successful response,
    in `warnings`.

    Conventions shared by every endpoint (identifiers, data formats,
    pagination, sparse updates, idempotency, rate limits, per-type support,
    and feature availability) live in the
    [API overview](/api-reference/overview). Tokens and scopes live in
    [Authentication](/api-reference/authentication).
servers:
  - url: https://api.abconvert.io/v1
security:
  - bearerAuth: []
tags:
  - name: Experiments
    description: Create, read, update, and list tests.
  - name: Lifecycle
    description: Status transitions. See the state machine in each action's description.
  - name: Results
    description: Read-only views of pipeline-computed snapshots. Beta.
  - name: Exports
    description: Async order-export jobs.
paths:
  /experiments:
    post:
      tags:
        - Experiments
      summary: Create a test
      description: >
        Creates a test in `draft`. Launching is a separate step: call

        [`POST
        /experiments/{id}/start`](/api-reference/lifecycle/launch-a-test).


        You can include `schedule` in the same call, so a single request

        creates a scheduled draft.


        What you can't create here: a visual editor test's DOM changes,

        `combined` tests, checkout block customizations, and offer widgets.

        You author those in the ABConvert admin, and they read back read-only.

        A `visual_editor` test itself is creatable, carrying custom CSS and

        JavaScript per test group.


        For the plan each test type needs, see

        [Feature availability](/api-reference/overview#feature-availability).
      operationId: createExperiment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExperimentCreate'
      responses:
        '201':
          description: The created draft. The response can carry non-blocking `warnings`.
          headers:
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExperimentWithWarnings'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        default:
          $ref: '#/components/responses/UnexpectedError'
      security:
        - bearerAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: |
        Replaying a request with the same key returns the original response
        instead of creating a second resource. Reusing a key with a different
        body returns 409 `idempotency_key_in_use`, and a key outside 1-255
        characters returns 400 `invalid_idempotency_key` rather than being
        ignored. See [Idempotency](/api-reference/overview#idempotency).
  schemas:
    ExperimentCreate:
      type: object
      additionalProperties: false
      required:
        - type
        - name
        - test_groups
      properties:
        type:
          $ref: '#/components/schemas/CreatableExperimentType'
        name:
          type: string
          maxLength: 255
        hypothesis:
          type: string
          description: What you expect this test to show, in your own words.
        primary_metric:
          $ref: '#/components/schemas/PrimaryMetric'
        traffic_allocation:
          type: integer
          minimum: 1
          maximum: 100
          default: 100
          description: The percentage of eligible visitors who enter the test.
        shared:
          allOf:
            - $ref: '#/components/schemas/SharedConfig'
          description: |
            Required on `price`, `shipping`, `template`, and `url_redirect`
            tests, which each carry a mandatory key here. Optional on `theme`,
            `offer`, and `checkout`. Omitting it where it is required returns
            422.
        test_groups:
          type: array
          minItems: 2
          maxItems: 5
          items:
            $ref: '#/components/schemas/TestGroupCreate'
        schedule:
          $ref: '#/components/schemas/Schedule'
        audience:
          $ref: '#/components/schemas/Audience'
    ExperimentWithWarnings:
      allOf:
        - $ref: '#/components/schemas/Experiment'
        - type: object
          properties:
            warnings:
              type: array
              description: Non-blocking findings. Absent when the request raised none.
              items:
                allOf:
                  - $ref: '#/components/schemas/Finding'
                  - type: object
                    properties:
                      severity:
                        type: string
                        const: warning
    CreatableExperimentType:
      type: string
      enum:
        - price
        - shipping
        - theme
        - template
        - url_redirect
        - checkout
        - offer
        - visual_editor
      description: >-
        The types you can create through the API. A `combined` test is authored
        in the ABConvert admin, with read and lifecycle access only.
    PrimaryMetric:
      type: string
      enum:
        - revenue_per_visitor
        - average_order_value
        - conversion_rate
        - profit_per_visitor
        - add_to_cart_rate
        - reached_checkout_rate
      example: revenue_per_visitor
      description: The metric the test is judged on.
    SharedConfig:
      type: object
      description: |
        Configuration shared by every test group in the test, keyed by
        change type. Send exactly the key that matches the test's `type`. A
        `combined` test carries one key per change type it bundles.
      properties:
        price:
          type: object
          required:
            - product_ids
          properties:
            product_ids:
              type: array
              minItems: 1
              maxItems: 500
              description: |
                One test can carry 100 products, or 500 on shops with the
                raised limit. Going over returns 422
                `product_limit_exceeded`, and the message names your cap and
                how many you sent.
              items:
                $ref: '#/components/schemas/ShopifyGid'
            markets:
              type: array
              description: |
                The markets you can set per-country prices in. Omitted =
                base price only. Listing a market makes the countries in
                its `countries` array priceable. Any country you price must
                be covered by a listed market, or the request returns 422
                `market_pricing_incomplete`. Countries are never dropped
                silently.
              items:
                $ref: '#/components/schemas/Market'
        shipping:
          type: object
          required:
            - zone_ids
          properties:
            zone_ids:
              type: array
              minItems: 1
              items:
                allOf:
                  - $ref: '#/components/schemas/ShopifyGid'
                example: gid://shopify/DeliveryZone/282141261
        theme:
          type: object
          description: Optional for theme tests. Carries URL scoping only.
          properties:
            path_rules:
              $ref: '#/components/schemas/PathRules'
        template:
          type: object
          required:
            - theme_id
            - template_type
          properties:
            theme_id:
              allOf:
                - $ref: '#/components/schemas/ShopifyGid'
              example: gid://shopify/OnlineStoreTheme/123456789
            template_type:
              $ref: '#/components/schemas/TemplateType'
            path_rules:
              $ref: '#/components/schemas/PathRules'
        url_redirect:
          type: object
          required:
            - rules
          properties:
            rules:
              type: array
              minItems: 1
              maxItems: 50
              description: |
                Trigger rules, shared by every test group. Each test group
                maps a destination to a rule by the rule's `key`. At most 50
                rules per test; more returns 422 `rule_limit_exceeded`.

                v1 reads and writes single-condition rules only. A test
                whose triggers use compound conditions, authored in the
                ABConvert admin, doesn't read back in full.
              items:
                $ref: '#/components/schemas/RedirectTriggerRule'
            redirect:
              $ref: '#/components/schemas/RedirectSettings'
        offer:
          type: object
          properties:
            combines_with:
              type: object
              description: |
                Which other discount classes the offer combines with. All
                three flags default to true.

                One restriction applies. An offer whose discount is a
                `shipping_discount` can't combine with other shipping
                discounts. ABConvert forces `shipping_discounts` to false
                for those offers, and sending true returns 422
                `discount_combination_invalid`.
              properties:
                product_discounts:
                  type: boolean
                  default: true
                order_discounts:
                  type: boolean
                  default: true
                shipping_discounts:
                  type: boolean
                  default: true
    TestGroupCreate:
      allOf:
        - $ref: '#/components/schemas/TestGroupBase'
        - type: object
          required:
            - split
            - changes
    Schedule:
      type: object
      additionalProperties: false
      minProperties: 1
      description: |
        The window the scheduler runs the test in. Send at least one bound.
        After launch, only `end_at` is editable.
      properties:
        start_at:
          type:
            - string
            - 'null'
          format: date-time
        end_at:
          type:
            - string
            - 'null'
          format: date-time
    Audience:
      type: object
      required:
        - filter_groups
      description: |
        Limits the test to visitors who match your filters. Without an
        `audience`, every visitor is eligible. In PATCH, `audience: null`
        turns targeting off.

        Filter groups combine with AND: a visitor must match every filter
        group to enter the test. Filters inside one filter group combine
        with OR: one match is enough.

        To require two filters at once, put each one in its own filter
        group. The example below enters visitors who are on mobile and new.
        Putting the same two filters in one filter group would enter
        visitors who are on mobile or new instead.

        A test whose targeting you turn off in the ABConvert admin reads
        back with no `audience`.
      example:
        filter_groups:
          - filters:
              - type: device
                operator: equals
                value: mobile
          - filters:
              - type: visitor_type
                operator: equals
                value: new
      properties:
        filter_groups:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - filters
            properties:
              filters:
                type: array
                minItems: 1
                items:
                  type: object
                  required:
                    - type
                    - operator
                  properties:
                    type:
                      type: string
                      enum:
                        - country
                        - market
                        - customer
                        - device
                        - visitor_type
                        - traffic_source
                        - referral_domain
                        - landing_page
                        - query_param
                        - cookie
                        - js
                      description: |
                        What the filter inspects. Every value is writable
                        except `js`, which runs storefront code you author in
                        the ABConvert admin. Sending `js` returns 422
                        `filter_type_unsupported`. On tests that already use
                        it, `js` reads back read-only.

                        `market` reads the Shopify market the visitor is
                        browsing. `customer` reads the logged-in customer's
                        identity. Both are in beta and need the ABConvert app
                        embed enabled on your theme. When the storefront can't
                        read the visitor's market or customer, ABConvert makes
                        no assignment on that page load rather than guessing,
                        and stores nothing, so the visitor can still enter on a
                        later load where the value reads.
                    key:
                      type: string
                      description: |
                        What the filter reads inside its `type`.

                        - `query_param` and `cookie`: the parameter or cookie
                          name, which you choose. Omitting it returns 422
                          `filter_key_required`.
                        - `customer`: one of `logged_in`, `tags`,
                          `orders_count`, `total_spent`. Omitting it, or
                          sending anything else, returns 422.

                        Other types don't read this field. PATCH doesn't
                        merge on it.
                    operator:
                      type: string
                      description: |
                        How `value` is compared. Which operators a filter
                        accepts depends on its `type`, and for `customer` on
                        its `key`. A pairing the storefront can't act on
                        returns 422 `filter_operator_invalid`, naming the
                        filter that carries it:

                        - `market` and `traffic_source`: `equals`,
                          `not_equals`, `in`, `not_in`.
                        - `customer` `logged_in`: `equals`.
                        - `customer` `tags`: `contains`, `not_contains`. To
                          match any of several tags, send one filter per tag
                          in the same filter group; filters in one filter
                          group are OR'd.
                        - `customer` `orders_count` and `total_spent`:
                          `greater_than`, `less_than`. The numeric readings
                          are the only ones the numeric operators apply to.
                        - Every other type: the ten operators from `equals`
                          through `regex`.

                        `exists` and `not_exists` apply only to
                        `query_param`, `cookie`, `referral_domain`, and
                        `customer` `tags`, the readings that can genuinely
                        be absent.
                      enum:
                        - equals
                        - not_equals
                        - contains
                        - not_contains
                        - in
                        - not_in
                        - exists
                        - not_exists
                        - starts_with
                        - regex
                        - greater_than
                        - less_than
                        - greater_or_equal
                        - less_or_equal
                    value:
                      description: |
                        What the filter compares against. The shape follows
                        the operator:

                        - Most operators: a string.
                        - `in` and `not_in`: an array of strings, or a single
                          string when you mean one.
                        - `exists` and `not_exists`: omit the field.

                        Sending an array to any other operator returns 422;
                        use `in` or `not_in` to match several values.

                        Values can't be empty, and `regex` patterns must
                        compile. Either violation returns 422
                        `filter_value_invalid`.

                        `equals`, `not_equals`, `in`, and `not_in` compare the
                        whole reading, so the value must be one the storefront
                        can report:

                        | Filter type | Accepted value |
                        |---|---|
                        | `country` | An uppercase ISO-3166 alpha-2 code |
                        | `device` | `desktop` or `mobile` |
                        | `visitor_type` | `new` or `returning` |
                        | `referral_domain` | A bare hostname |
                        | `market` | The market handle from Settings > Markets |

                        Under `contains`, `starts_with`, and `regex` the value
                        is a fragment of the reading, so only the emptiness and
                        compile rules apply.

                        A `customer` filter's value follows its `key`, and is
                        always sent as a string:

                        - `logged_in`: `"true"` or `"false"`.
                        - `orders_count` and `total_spent`: a number, such as
                          `"3"` or `"99.50"`. `total_spent` is in your store's
                          currency.
                        - `tags`: one Shopify customer tag. Matching is exact
                          and case-insensitive, so `wholesale` doesn't match
                          `wholesale-pending`.

                        Anything the storefront couldn't read as that shape
                        returns 422 `filter_value_invalid`.

                        A `traffic_source` outside the channels and platforms
                        ABConvert classifies is accepted with a
                        `filter_value_unrecognized` warning: a raw
                        `utm_source` reaches the storefront verbatim, so it
                        can still match.
                      oneOf:
                        - title: Single value
                          type: string
                        - title: Value list
                          type: array
                          items:
                            type: string
    Experiment:
      type: object
      required:
        - object
        - id
        - type
        - status
        - name
        - test_groups
        - created_at
        - updated_at
      properties:
        object:
          type: string
          const: experiment
        id:
          type: string
          example: '3021'
          description: The test's numeric ID as a string.
        type:
          $ref: '#/components/schemas/ExperimentType'
        status:
          $ref: '#/components/schemas/ExperimentStatus'
        name:
          type: string
        hypothesis:
          type:
            - string
            - 'null'
        primary_metric:
          oneOf:
            - $ref: '#/components/schemas/PrimaryMetric'
            - type: 'null'
        traffic_allocation:
          type: integer
        shared:
          $ref: '#/components/schemas/SharedConfig'
        test_groups:
          type: array
          items:
            $ref: '#/components/schemas/TestGroup'
        schedule:
          oneOf:
            - $ref: '#/components/schemas/Schedule'
            - type: 'null'
        audience:
          oneOf:
            - $ref: '#/components/schemas/Audience'
            - type: 'null'
        has_force_assign_rules:
          type: boolean
          readOnly: true
          description: |
            True when the test has force-assign rules, which you author in
            the ABConvert admin. ABConvert evaluates them only for visitors
            who already match `audience`, and applies the first rule whose
            filters all match: that rule either puts the visitor in the test
            group it names or keeps them out of the test, in place of the
            random split.
        assignment_version:
          type: integer
          readOnly: true
          description: >-
            Increments whenever visitors are reassigned to test groups, for
            example after a split change.
        product_group:
          type:
            - object
            - 'null'
          readOnly: true
          description: |
            The product group attached to this test, or null. Its `id` is what
            `product_group_id` takes on a custom result query. No
            product-group endpoint exists, so read it here.
          properties:
            id:
              type: string
              example: 66c1f0a2e4b0a1d2c3e4f5a6
            name:
              type: string
              example: Bestsellers
        results_summary:
          readOnly: true
          description: |
            Present only with `?include=results_summary`. Null when no
            snapshot has been computed for the test yet.
          oneOf:
            - $ref: '#/components/schemas/ResultsSummary'
            - type: 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        started_at:
          type:
            - string
            - 'null'
          format: date-time
        ended_at:
          type:
            - string
            - 'null'
          format: date-time
    Finding:
      type: object
      required:
        - severity
        - code
        - message
      properties:
        severity:
          type: string
          enum:
            - error
            - warning
        code:
          type: string
          description: |
            A snake_case identifier for the finding. Published codes never
            change, and new ones are only added.
          enum:
            - product_not_found
            - collection_not_found
            - theme_not_found
            - template_not_found
            - zone_not_found
            - rate_not_found
            - market_not_found
            - rule_key_not_found
            - template_required
            - destination_required
            - change_required
            - rate_required
            - offer_required
            - theme_required
            - market_empty
            - product_not_testable
            - theme_not_testable
            - duplicate_change
            - duplicate_group_name
            - duplicate_rule_key
            - duplicate_trigger_url
            - duplicate_theme
            - duplicate_template
            - control_group_invalid
            - control_offers_rates
            - control_redirects
            - control_runs_code
            - split_sum_invalid
            - test_groups_too_many
            - zone_not_declared
            - discount_combination_invalid
            - currency_mismatch
            - match_type_unsupported
            - max_discount_amount_not_allowed
            - condition_range_invalid
            - condition_unit_invalid
            - product_limit_exceeded
            - country_limit_exceeded
            - rule_limit_exceeded
            - market_pricing_incomplete
            - path_invalid
            - rate_name_required
            - rate_price_invalid
            - offer_title_required
            - filter_value_invalid
            - filter_operator_invalid
            - filter_key_required
            - filter_type_unsupported
            - custom_js_invalid
            - split_changed
            - country_served_by_other_market
            - country_ownership_ambiguous
            - market_not_contextual
            - trigger_overlaps
            - destination_matches_trigger
            - zone_not_configured
            - theme_not_published
            - filter_value_unrecognized
            - test_groups_missing
            - split_invalid
            - theme_test_running
            - visual_editor_conflict
            - app_embed_disabled
            - resource_claimed
            - test_overrides_personalization
            - audience_empty
            - force_assign_group_invalid
            - block_placement_invalid
            - modification_invalid
            - modification_kind_unknown
            - modification_duplicate_singleton
            - launch_check_failed
        param:
          type:
            - string
            - 'null'
          description: |
            JSON path of the field the finding points at, for example
            `shared.price.markets[0].countries[2]`. Offending values appear
            in `message`, not here.
        message:
          type: string
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - type
            - code
            - message
          properties:
            type:
              type: string
              enum:
                - invalid_request_error
                - authentication_error
                - permission_error
                - not_found_error
                - conflict_error
                - validation_error
                - rate_limit_error
                - api_error
            code:
              type: string
              description: |
                The code set is fixed for each error type.

                - 400: `malformed_json`, `unknown_parameter`,
                  `invalid_cursor`, `invalid_include`,
                  `invalid_idempotency_key` (an `Idempotency-Key` header that
                  is empty or over 255 characters), `invalid_request`
                  (a query parameter sent more than once, or `limit` not
                  a whole number between 1 and 100)
                - 401: `missing_token`, `invalid_token`, `token_revoked`
                - 403: `insufficient_scope`, `api_access_disabled`,
                  `subscription_inactive`, `billing_cap_reached`,
                  `feature_not_in_plan`, `carrier_service_required`
                - 404: `experiment_not_found`, `export_not_found`,
                  `results_query_not_found`
                - 409: `invalid_status_transition`, `locked_field`,
                  `idempotency_key_in_use`, `checks_pending`,
                  `editor_owned_content`
                - 413: `request_too_large` (the body is over 10 MB)
                - 422: read the `findings` array. Each entry's `param` names
                  the field and its `message` names what to fix; the top-level
                  `code` repeats the first blocking finding. Some codes aren't
                  finding rules:
                  `invalid_request` (a body or parameter the schema refuses),
                  `invalid_type` (a `type` this API can't create),
                  `invalid_configuration` (the test as stored isn't valid to
                  act on; `message` names what to fix),
                  `launch_blocked` (a launch check failed with no finding to
                  name), `preview_unsupported` (this test can't show a single
                  test group through a preview link; preview it in the
                  ABConvert admin), and
                  `export_not_available` (the test hasn't run yet, so there is
                  nothing to export).
                  A custom result query refuses its own parameters
                  with `invalid_breakdown_dimension`,
                  `breakdown_too_many_dimensions`,
                  `duplicate_breakdown_dimension`, `invalid_date_range`,
                  `date_range_out_of_bounds` (the window ends before the test
                  started, or begins after today), `sample_basis_unsupported`,
                  `product_group_not_found`,
                  `product_id_requires_product_group`, or
                  `results_not_available` (the test hasn't run yet).

                  A query parameter value outside the published vocabulary
                  (`status`, `type`, `scheduled`, a `created_at` bound) returns
                  `invalid_request` with `param` naming the parameter.

                  On create, the codes fall into four families:
                  - a resource this shop doesn't have:
                    `product_not_found`, `collection_not_found`,
                    `theme_not_found`, `template_not_found`,
                    `zone_not_found`, `rate_not_found`
                  - a test group with nothing to test, so add a change:
                    `template_required`,
                    `destination_required`, `change_required`,
                    `rate_required`, `offer_required`
                  - two parts of the request disagree, and the code names
                    which:
                    `duplicate_change`, `duplicate_group_name`,
                    `duplicate_rule_key`, `rule_key_not_found`,
                    `zone_not_declared`, `control_group_invalid`,
                    `split_sum_invalid`
                  - a value this API won't write, so author it in the
                    ABConvert admin instead:
                    `currency_mismatch`, `control_offers_rates`,
                    `match_type_unsupported`,
                    `max_discount_amount_not_allowed`,
                    `discount_combination_invalid`,
                    `condition_range_invalid`, `product_limit_exceeded`,
                    `market_pricing_incomplete`
                - 429: `rate_limited`
                - 500: `internal_error`
                - 501: `not_implemented` (a test type this API can't create
                  yet, or a test whose stored form this API can't read back;
                  use the ABConvert admin instead)
                - 503: `rate_limit_unavailable` (the limiter could not be
                  reached, so the write was refused rather than let through
                  uncounted; reads are served in this case),
                  `audit_unavailable` (the write could not be recorded in the
                  audit log, so it was not performed),
                  `idempotency_unavailable` (ABConvert could not check your
                  `Idempotency-Key`, so the request was refused rather than run
                  without replay protection; retry with the same key),
                  `internal_error` (the request could not be run alongside
                  other changes to this shop, so nothing was attempted; retry)
                - 504: `internal_error` (the write took too long and was rolled
                  back, so nothing changed)

                Every 5xx carries a `request_id`. Quote it when you contact
                support.
            message:
              type: string
            param:
              type:
                - string
                - 'null'
              description: |
                JSON path of the offending request field. On
                `feature_not_in_plan` it carries the feature slug instead:
                `price`, `offer`, `multi_market`, or `checkout_blocks`.
            request_id:
              type: string
              description: |
                Correlation id for one refused request. Present on every 5xx
                and on nothing else: a 4xx is about the request, and there is
                nothing on our side to look up. Send an `x-correlation-id`
                header and that value is the id you get back.
            details:
              type: object
              additionalProperties: true
              description: |
                Machine-readable context, present where a code defines it.
                `invalid_status_transition` carries
                `{"current_status": "...", "allowed_actions": [...]}` so a
                client can branch without parsing `message`.
            findings:
              type: array
              description: Present on `validation_error`.
              items:
                $ref: '#/components/schemas/Finding'
    ShopifyGid:
      type: string
      pattern: ^gid://shopify/[A-Za-z]+/\d+$
      example: gid://shopify/Product/8123456789
      description: >-
        A Shopify global ID (GID). It is the only accepted form for Shopify
        resources.
    Market:
      type: object
      description: |
        A Shopify market and the countries you price in it.

        `countries` is optional on write and always present on read. Omit it and
        the server resolves the countries this market owns and pins that list on
        the test, so a later change to your Shopify markets can't move what the
        test prices. Send it to narrow that set; anything you send that the
        market doesn't own returns 422 `market_pricing_incomplete` rather than
        being dropped.

        Shopify serves a buyer from the most specific market containing their
        country, so a country covered by a narrower market belongs to that one.
        ABConvert reports every country it leaves out as a warning, one per
        reason:

        - `country_served_by_other_market`: a narrower market already serves it.
        - `country_ownership_ambiguous`: more than one market claims it.
        - `market_not_contextual`: the market transacts in your shop's own
          currency, so the base price already reaches its buyers.

        A market that ends up owning nothing returns 422 `market_empty`.
      required:
        - id
      properties:
        id:
          allOf:
            - $ref: '#/components/schemas/ShopifyGid'
          example: gid://shopify/Market/13762560
        countries:
          type: array
          minItems: 1
          description: |
            ISO 3166-1 alpha-2 codes for the countries this market covers.
            Optional on write; always present on read, where it is the pinned
            list the test actually prices.
          items:
            type: string
            pattern: ^[A-Z]{2}$
    PathRules:
      type: object
      required:
        - mode
        - rules
      description: |
        Limits the test to certain storefront URLs. Omitted = storewide.
        Rules combine with OR: a page matches when any one rule matches.
      properties:
        mode:
          type: string
          enum:
            - include
            - exclude
          description: |
            `include` applies the test on matching pages only. `exclude`
            applies it everywhere except matching pages.
        rules:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - match_type
              - match_value
            properties:
              subject:
                $ref: '#/components/schemas/MatchSubject'
              match_type:
                type: string
                enum:
                  - exact
                  - starts_with
                  - contains
                  - ends_with
                  - regex
                  - not_exact
                  - not_starts_with
                  - not_ends_with
                  - not_contains
                  - not_regex
                description: >-
                  Every value is writable here, unlike in
                  `shared.url_redirect.rules`.
              match_value:
                type: string
                minLength: 1
                example: /collections/sale
    TemplateType:
      type: string
      enum:
        - index
        - collection
        - product
        - page
        - blog
        - article
        - cart
        - search
      example: product
    RedirectTriggerRule:
      type: object
      required:
        - key
        - match_type
        - match_value
      properties:
        key:
          type: string
          minLength: 1
          description: |
            Identifies this rule within the request: each test group's
            `destinations` entry points at it by `rule_key`.

            The key you send is not stored. The server publishes its own
            keys on read (`rule_1`, `rule_2`, and so on, in the order the
            rules appear) and every response carries them, so read the keys off
            the create response before addressing destinations in a PATCH.
            Like test-group indexes, they can renumber when the rule list
            changes; re-read the test before further key-addressed edits.
        subject:
          allOf:
            - $ref: '#/components/schemas/MatchSubject'
          description: |
            Writes accept `path` only. A redirect trigger is stored without
            a subject, so any other value could only be dropped silently and
            is refused instead. Tests authored in the ABConvert admin can
            match on the other subjects, and those read back.
        match_type:
          type: string
          enum:
            - exact
            - starts_with
            - contains
            - ends_with
            - regex
            - not_exact
            - not_starts_with
            - not_ends_with
            - not_contains
            - not_regex
          description: |
            How `match_value` is compared. You can write `exact`,
            `starts_with`, and `contains`. Any other value returns 422
            `match_type_unsupported`.

            Tests authored in the ABConvert admin can use the full list, and
            those values read back read-only. Analytics for them may fall
            back to storewide matching.
        match_value:
          type: string
          minLength: 1
          description: |
            The pattern to match. `match_type` decides how it is compared,
            and `subject` decides what it is compared against. The match
            never includes the origin.
          example: /products/old-pdp
    RedirectSettings:
      type: object
      properties:
        mode:
          type: string
          enum:
            - always
            - once
          default: always
          description: >-
            How often the redirect fires. `always` redirects every matching
            visit. `once` redirects only the first one.
        preserve_query_string:
          type: boolean
          default: true
          description: Carries the incoming query string over to the destination URL.
    TestGroupBase:
      type: object
      description: |
        A test group's identity is its position in `test_groups`. The first
        entry is index 0. Results, preview URLs, and keyed merges in PATCH
        all reference that index, so `name` is free to change at any time.
      properties:
        name:
          type: string
          example: Variant A
          description: |
            The name shown for this test group in the ABConvert admin.
            Defaults from the index (`Control`, `Variant A`, `Variant B`) and
            stays editable after
            launch. Unique within the test; a duplicate returns 422
            `duplicate_group_name`. Uniqueness is checked against the name
            each test group ends up with, so sending `Variant A` for one
            test group collides with the default another would be given.
        control:
          type: boolean
          default: false
          description: |
            Exactly one test group must set this. None, or more than one,
            returns 422 `control_group_invalid`.
        split:
          type: integer
          minimum: 0
          maximum: 100
          description: |
            Splits across test groups must sum to 100, or the request
            returns 422 `split_sum_invalid`.
        changes:
          type: array
          items:
            $ref: '#/components/schemas/Change'
    ExperimentType:
      type: string
      enum:
        - price
        - shipping
        - theme
        - template
        - url_redirect
        - checkout
        - offer
        - visual_editor
        - combined
      description: |
        What the test changes: product prices, shipping rates, the theme a
        visitor sees, a page template, the URL a visitor lands on, checkout
        delivery and payment methods, or a cart offer. `visual_editor` is a
        storefront edit: custom CSS and JavaScript, which you can write here,
        alongside DOM changes authored in the ABConvert visual editor.
        `combined` bundles more than one change type in one test. See
        `CreatableExperimentType` for the ones you can create here.
    ExperimentStatus:
      type: string
      enum:
        - draft
        - preview
        - active
        - paused
        - ended
        - failed
        - archived
      description: |
        The test's current status. The ABConvert admin labels `ended` as
        **Closed**; the wire value stays `ended`. Scheduling isn't a status: a
        scheduled test is a `draft` or `preview` with a `schedule`.
    TestGroup:
      description: |
        `changes` is present on every single-test read and absent from
        [`GET /experiments`](/api-reference/experiments/list-tests), which
        returns test group identity only. Fetch
        a test by id to read its changes.
      allOf:
        - $ref: '#/components/schemas/TestGroupBase'
        - type: object
          required:
            - name
            - control
            - split
          properties:
            template_preview_url:
              type:
                - string
                - 'null'
              readOnly: true
              description: Plain storefront link, no test assignment. Template tests only.
    ResultsSummary:
      type: object
      description: >
        The small fixed summary `?include=results_summary` inlines. It carries

        no lift, p-values, or intervals: read those from

        [`GET
        /experiments/{id}/results`](/api-reference/results/retrieve-the-results-snapshot).


        Beta: the shape may change. It inlines `TestGroupMetrics`, so it

        tracks the snapshot's metric shapes.
      required:
        - computed_at
        - test_groups
      properties:
        computed_at:
          type: string
          format: date-time
        outcome:
          type:
            - string
            - 'null'
          enum:
            - winner
            - loser
            - inconclusive
            - insufficient_data
            - null
        winning_test_group_index:
          type:
            - integer
            - 'null'
        srm_status:
          type:
            - string
            - 'null'
          enum:
            - ok
            - mismatch
            - insufficient_data
            - null
        test_groups:
          type: array
          description: Sample size and the six primary metrics, per test group.
          items:
            $ref: '#/components/schemas/TestGroupMetrics'
    MatchSubject:
      type: string
      enum:
        - path
        - path_and_query_string
        - query_string
      default: path
      description: |
        Which part of the URL the rule matches against. The origin is never
        part of the match.

        - `path`: the page path relative to the store root, starting with `/`.
        - `path_and_query_string`: the path with the query string appended.
        - `query_string`: the query string alone, including the leading `?`.

        `query_string` changes how some match types behave. `exact` and
        `contains`, and their negations, compare parameter pairs instead of
        raw text: `exact` requires the same parameter set with no extras,
        and `contains` requires every listed pair to be present. Write
        `match_value` as one or more parameter pairs, such as
        `utm_source=spring`.

        `starts_with`, `ends_with`, and `regex` still compare the raw query
        string. Avoid them with this subject.
    Change:
      description: |
        One change applied by a test group. `changes: []` means "change
        nothing", which is the usual control.

        Any test group can carry changes, including the control, where the
        test type supports it. Per-type validation decides whether a given
        control change is meaningful.
      oneOf:
        - $ref: '#/components/schemas/PriceChange'
        - $ref: '#/components/schemas/ShippingChange'
        - $ref: '#/components/schemas/ThemeChange'
        - $ref: '#/components/schemas/TemplateChange'
        - $ref: '#/components/schemas/UrlRedirectChange'
        - $ref: '#/components/schemas/CheckoutChange'
        - $ref: '#/components/schemas/OfferChange'
        - $ref: '#/components/schemas/CheckoutBlockChange'
        - $ref: '#/components/schemas/VisualEditorChange'
      discriminator:
        propertyName: type
        mapping:
          price:
            $ref: '#/components/schemas/PriceChange'
          shipping:
            $ref: '#/components/schemas/ShippingChange'
          theme:
            $ref: '#/components/schemas/ThemeChange'
          template:
            $ref: '#/components/schemas/TemplateChange'
          url_redirect:
            $ref: '#/components/schemas/UrlRedirectChange'
          checkout:
            $ref: '#/components/schemas/CheckoutChange'
          offer:
            $ref: '#/components/schemas/OfferChange'
          checkout_block:
            $ref: '#/components/schemas/CheckoutBlockChange'
          visual_editor:
            $ref: '#/components/schemas/VisualEditorChange'
    TestGroupMetrics:
      type: object
      required:
        - test_group_index
        - sample_size
      properties:
        test_group_index:
          type: integer
          example: 1
        sample_size:
          type: integer
          example: 5231
          description: |
            Visitors in the test group, or exposures for exposure-measured
            types. This is the denominator for `conversion_rate`,
            `revenue_per_visitor`, and `profit_per_visitor`.
        session_count:
          type:
            - integer
            - 'null'
          example: 6816
          description: |
            Sessions from the test group's traffic. This is the denominator
            for `add_to_cart_rate` and `reached_checkout_rate`.
        conversion_rate:
          type:
            - number
            - 'null'
          example: 0.034
        revenue_per_visitor:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
          description: |
            Money, in the shop's own currency.
        average_order_value:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        profit_per_visitor:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
          description: Null until COGS settings are configured.
        add_to_cart_rate:
          type:
            - number
            - 'null'
          example: 0.081
        reached_checkout_rate:
          type:
            - number
            - 'null'
          example: 0.052
    PriceChange:
      title: Price change
      type: object
      required:
        - type
      anyOf:
        - required:
            - adjustment
        - required:
            - prices
      description: |
        Sets test prices with an adjustment, with exact prices, or with
        both. Send at least one of `adjustment` and `prices`. When both
        cover the same price, the exact price wins.

        ABConvert resolves the adjustment to absolute prices when you save
        the test. Those prices don't follow later price changes in Shopify.

        `adjustment` is an instruction, not a stored setting. Any request
        carrying it recalculates every price you don't send in that same
        request, including prices you set by hand earlier. To change one
        price and keep the rest, send `prices` without `adjustment`.
      properties:
        type:
          type: string
          const: price
        adjustment:
          $ref: '#/components/schemas/Adjustment'
        prices:
          type: array
          description: |
            Exact prices, one entry per product variant.

            `price` is the base price. Omit it to let `adjustment` set the
            base. `country_prices` holds per-country overrides.

            PATCH merges entries by `product_variant_id`, and merges
            `country_prices` entries by `country`. The exception is a change
            carrying `adjustment`, which replaces the whole list instead.
          example:
            - product_variant_id: gid://shopify/ProductVariant/45678
              price:
                amount: '17.99'
                currency: USD
              compare_at_price:
                amount: '24.99'
                currency: USD
              country_prices:
                - country: AU
                  price:
                    amount: '27.99'
                    currency: AUD
          items:
            type: object
            required:
              - product_variant_id
            properties:
              product_variant_id:
                allOf:
                  - $ref: '#/components/schemas/ShopifyGid'
                example: gid://shopify/ProductVariant/45678
              price:
                allOf:
                  - $ref: '#/components/schemas/Money'
                description: The base price. Omitted = derived from the adjustment.
              compare_at_price:
                allOf:
                  - $ref: '#/components/schemas/Money'
                description: Omitted = untouched.
              remove:
                type: boolean
                writeOnly: true
                description: >-
                  Deletes this entry. PATCH only. Sending it on create returns
                  400 `unknown_parameter`.
              country_prices:
                type: array
                description: |
                  Per-country overrides. Every country here must be covered
                  by `shared.price.markets`, or the request returns 422
                  `market_pricing_incomplete`.
                items:
                  type: object
                  required:
                    - country
                  anyOf:
                    - title: Set price
                      required:
                        - price
                    - title: Remove entry
                      required:
                        - remove
                      properties:
                        remove:
                          const: true
                  properties:
                    country:
                      type: string
                      pattern: ^[A-Z]{2}$
                      example: AU
                    price:
                      $ref: '#/components/schemas/Money'
                    compare_at_price:
                      allOf:
                        - $ref: '#/components/schemas/Money'
                      description: Omitted = untouched.
                    remove:
                      type: boolean
                      writeOnly: true
                      description: >-
                        Deletes this entry. PATCH only. Sending it on create
                        returns 400 `unknown_parameter`.
    ShippingChange:
      title: Shipping change
      type: object
      required:
        - type
        - offered_rates
      description: |
        Each test group lists the rates it offers, per delivery zone. A rate
        you leave out isn't offered to that test group.

        The control shows the zone's own rates and sends `changes: []`.
        Offering rates on the control returns 422 `control_offers_rates`,
        because they would be stored and read back but never served.

        - `shopify_rate_id` alone keeps an existing rate as-is.
        - `shopify_rate_id` with other fields changes that rate.
        - An entry with no `shopify_rate_id` is a new rate.
      properties:
        type:
          type: string
          const: shipping
        offered_rates:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - zone_id
              - rates
            properties:
              zone_id:
                allOf:
                  - $ref: '#/components/schemas/ShopifyGid'
                example: gid://shopify/DeliveryZone/282141261
              rates:
                type: array
                items:
                  oneOf:
                    - title: Shopify rate
                      type: object
                      required:
                        - shopify_rate_id
                      description: >-
                        Keeps an existing Shopify rate as-is. Add any other
                        field to change it.
                      properties:
                        shopify_rate_id:
                          allOf:
                            - $ref: '#/components/schemas/ShopifyGid'
                          example: gid://shopify/DeliveryMethodDefinition/789
                        name:
                          type: string
                        price:
                          $ref: '#/components/schemas/Money'
                        condition:
                          $ref: '#/components/schemas/RateCondition'
                        transit_time:
                          $ref: '#/components/schemas/TransitTime'
                    - title: New rate
                      type: object
                      required:
                        - name
                        - price
                      not:
                        required:
                          - shopify_rate_id
                      properties:
                        name:
                          type: string
                          minLength: 1
                          example: Express
                        price:
                          $ref: '#/components/schemas/Money'
                        condition:
                          $ref: '#/components/schemas/RateCondition'
                        transit_time:
                          $ref: '#/components/schemas/TransitTime'
    ThemeChange:
      title: Theme change
      type: object
      required:
        - type
        - theme_id
      properties:
        type:
          type: string
          const: theme
        theme_id:
          allOf:
            - $ref: '#/components/schemas/ShopifyGid'
          example: gid://shopify/OnlineStoreTheme/987654321
          description: |
            The theme this test group shows. On the control, sending
            `changes: []` means the main theme; it is resolved at create
            time and reads back as an explicit `theme_id`, which is the
            theme a visitor will actually be shown. An explicit value pins a
            specific theme.
    TemplateChange:
      title: Template change
      type: object
      required:
        - type
        - template_key
      description: |
        Unlike other types, a template test's control carries a change too:
        it names the template it shows, usually the type's default. A test
        group sending `changes: []` returns 422 `template_required`, because
        the server can't pick a template on its behalf.
      properties:
        type:
          type: string
          const: template
        template_key:
          type: string
          minLength: 1
          description: >-
            The template file key, for example `product.summer.json`. ABConvert
            builds the `template_preview_url` for this test group from it.
    UrlRedirectChange:
      title: URL redirect change
      type: object
      required:
        - type
        - destinations
      properties:
        type:
          type: string
          const: url_redirect
        destinations:
          type: array
          minItems: 1
          description: One destination per rule key in `shared.url_redirect.rules`.
          items:
            type: object
            required:
              - rule_key
              - url
            properties:
              rule_key:
                type: string
                minLength: 1
                description: >-
                  The `key` of the rule in `shared.url_redirect.rules` this
                  destination answers.
              url:
                type: string
                minLength: 1
                description: |
                  Where a matching visitor is sent: a path on this store,
                  starting with `/`. A full URL, anything containing `//` or
                  `:`, or any whitespace returns 422 `path_invalid`. A
                  destination identical to an `exact` trigger's `match_value`
                  returns 422 `destination_matches_trigger`.
                example: /products/new-pdp
    CheckoutChange:
      title: Checkout change
      description: |
        One checkout customization per change entry. `action` selects the
        branch, and each branch carries only its own fields.

        Delivery and payment customizations only. Checkout block
        customizations are authored in the ABConvert admin and appear
        read-only.

        `has_conditions` is true when the customization has display
        conditions attached in the ABConvert admin, such as cart value,
        customer tag, country, or device.
      oneOf:
        - title: 'Checkout: hide methods'
          type: object
          required:
            - type
            - target
            - action
            - method_names
          properties:
            type:
              type: string
              const: checkout
            target:
              type: string
              enum:
                - delivery
                - payment
            action:
              type: string
              const: hide
            method_names:
              type: array
              minItems: 1
              items:
                type: string
                minLength: 1
            has_conditions:
              type: boolean
              readOnly: true
        - title: 'Checkout: reorder methods'
          type: object
          required:
            - type
            - target
            - action
            - pin_rules
          properties:
            type:
              type: string
              const: checkout
            target:
              type: string
              enum:
                - delivery
                - payment
            action:
              type: string
              const: reorder
            pin_rules:
              type: array
              minItems: 1
              description: Pins matching methods to the top of the list, in rule order.
              items:
                type: object
                required:
                  - match_type
                  - value
                properties:
                  match_type:
                    type: string
                    enum:
                      - exact
                      - contains
                  value:
                    type: string
                    example: Shop Pay
            has_conditions:
              type: boolean
              readOnly: true
        - title: 'Checkout: rename methods'
          type: object
          required:
            - type
            - target
            - action
            - rename_rules
          properties:
            type:
              type: string
              const: checkout
            target:
              type: string
              enum:
                - delivery
                - payment
            action:
              type: string
              const: rename
            rename_rules:
              type: array
              minItems: 1
              description: Each rule matches a method name and rewrites it.
              items:
                type: object
                required:
                  - match_type
                  - value
                  - mode
                  - to
                properties:
                  match_type:
                    type: string
                    enum:
                      - exact
                      - contains
                  value:
                    type: string
                    description: The method-name pattern to match.
                  mode:
                    type: string
                    enum:
                      - replace
                      - append
                      - prepend
                  to:
                    type: string
                    description: >-
                      The new text. `mode` decides whether it replaces the
                      matched name, appends to it, or prepends to it.
            has_conditions:
              type: boolean
              readOnly: true
    OfferChange:
      title: Offer change
      type: object
      required:
        - type
        - title
        - discounts
      description: >-
        The offer this test group shows. The API never accepts widgets, so the
        server writes them empty.
      properties:
        type:
          type: string
          const: offer
        title:
          type: string
          minLength: 1
        has_widgets:
          type: boolean
          readOnly: true
          description: >-
            True when the offer carries storefront widgets authored in the
            ABConvert admin.
        discounts:
          type: array
          minItems: 1
          maxItems: 1
          description: |
            Exactly one discount per test group in v1. It is an array so
            that multi-discount offers can be added later without a
            breaking change.
          items:
            oneOf:
              - title: Product discount
                type: object
                required:
                  - type
                  - value
                properties:
                  type:
                    type: string
                    const: product_discount
                  value:
                    $ref: '#/components/schemas/DiscountValue'
                  scope:
                    $ref: '#/components/schemas/DiscountScope'
                  condition:
                    $ref: '#/components/schemas/DiscountCondition'
              - title: Order discount
                type: object
                required:
                  - type
                  - value
                properties:
                  type:
                    type: string
                    const: order_discount
                  value:
                    $ref: '#/components/schemas/DiscountValue'
                  condition:
                    $ref: '#/components/schemas/DiscountCondition'
              - title: Shipping discount
                type: object
                required:
                  - type
                  - value
                properties:
                  type:
                    type: string
                    const: shipping_discount
                  value:
                    $ref: '#/components/schemas/DiscountValue'
                  scope:
                    $ref: '#/components/schemas/ShippingDiscountScope'
                  condition:
                    $ref: '#/components/schemas/DiscountCondition'
              - title: Volume discount
                type: object
                readOnly: true
                required:
                  - type
                  - tiers
                description: |
                  Authored in the ABConvert admin. Read-only. Tier
                  thresholds are item counts.
                properties:
                  type:
                    type: string
                    const: volume_discount
                  tiers:
                    type: array
                    minItems: 1
                    items:
                      $ref: '#/components/schemas/VolumeTier'
                  scope:
                    $ref: '#/components/schemas/DiscountScope'
                  condition:
                    $ref: '#/components/schemas/DiscountCondition'
              - title: Threshold discount
                type: object
                readOnly: true
                required:
                  - type
                  - tiers
                description: |
                  Authored in the ABConvert admin. Read-only. Tier
                  thresholds are order subtotals.
                properties:
                  type:
                    type: string
                    const: threshold_discount
                  tiers:
                    type: array
                    minItems: 1
                    items:
                      $ref: '#/components/schemas/ThresholdTier'
                  condition:
                    $ref: '#/components/schemas/DiscountCondition'
    CheckoutBlockChange:
      title: Checkout block change
      type: object
      readOnly: true
      description: |
        A checkout block customization, authored in the ABConvert admin.
        Read-only.
      properties:
        type:
          type: string
          const: checkout_block
        placement:
          type: string
          description: Where in checkout the block renders.
        block_type:
          type: string
    VisualEditorChange:
      title: Visual editor change
      type: object
      required:
        - type
      description: |
        What one test group changes on the storefront.

        The custom code is yours to write: send `custom_css`, `custom_js`, or
        both, and they read back as sent, in full, to any token that can read
        the test — `read_experiments` included. The DOM changes are made by
        pointing at elements in the ABConvert admin's visual editor, so the API
        never accepts them and publishes a `summary` of them instead.

        Both code blocks run on every page the test runs on, and the test's
        pages are set in the ABConvert admin. Scope the code to one page from
        inside it, for example by returning early unless `location.pathname`
        matches. Timing is not configurable: the CSS and then the JavaScript
        are injected the moment the test applies to the page, before the DOM
        changes, and the JavaScript runs as an inline script.

        A change carrying neither code block is a test group whose changes
        were all made in the visual editor.

        The control test group cannot carry either block. Code that runs for
        the control runs for every visitor in the test, leaving the variants
        nothing to be measured against, so it is refused with 422
        `control_runs_code`.
      properties:
        type:
          type: string
          const: visual_editor
        custom_css:
          type: string
          maxLength: 50000
          example: '.hero__title { font-size: 2.5rem; }'
          description: |
            A stylesheet, injected as a `<style>` block. Not validated: a rule
            the browser can't parse is dropped on its own, without taking the
            rest of the block with it.
        custom_js:
          type: string
          maxLength: 50000
          example: >-
            document.querySelector(".hero__title").textContent = "Free shipping
            over $50";
          description: |
            A script, injected as an inline script tag. It has to parse: a
            syntax error means the browser runs none of it, so one is refused
            at write time with 422 `custom_js_invalid`. Nothing beyond parsing
            is checked, the same standard the visual editor's own **Add code**
            panel holds it to.
        summary:
          type: object
          readOnly: true
          description: >-
            The DOM changes this test group makes, which are authored in the
            ABConvert admin.
          properties:
            change_count:
              type: integer
            changes_by_action:
              type: object
              description: >-
                Count of changes by visual editor edit type: `set`, `move`,
                `remove`.
              additionalProperties:
                type: integer
            changed_attributes:
              type: array
              items:
                type: string
            has_custom_css:
              type: boolean
            has_custom_js:
              type: boolean
            custom_js_sha256:
              type: string
              description: >-
                Digest of `custom_js`, for clients that only need to notice that
                it changed.
    Money:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: string
          pattern: ^-?\d+(\.\d+)?$
          example: '17.99'
          description: >-
            A decimal string. Only fields that express a delta accept a leading
            minus sign.
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          example: USD
          description: |
            ISO 4217 code. On write it must be the shop's own currency;
            anything else returns 422 `currency_mismatch`.
    Adjustment:
      type: object
      description: |
        Applies one change to every price in the test's scope, for example
        "decrease all prices by 10%". The sign sets the direction. An exact
        price on a product variant overrides the adjustment for that variant.
      oneOf:
        - title: Percentage
          type: object
          required:
            - unit
            - value
          properties:
            unit:
              type: string
              const: percentage
            value:
              type: number
              minimum: -100
              maximum: 100
              description: |
                Signed percent. `-10` decreases prices by 10%. Writes accept
                integers. Tests authored in the ABConvert admin can read
                back decimals.
        - title: Fixed amount
          type: object
          required:
            - unit
            - amount
          properties:
            unit:
              type: string
              const: fixed
            amount:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: |
                A signed money delta. `{"amount": "-5.00", "currency": "USD"}`
                takes 5.00 off each price.
    RateCondition:
      description: |
        Limits when the rate is offered, by order price or order weight.
        Both bounds are optional. When you send both, `minimum` must not
        exceed `maximum` and the two must share one currency, or the
        request returns 422 `condition_range_invalid`. The same code covers
        a `transit_time` whose `max_days` is less than its `min_days`.
      oneOf:
        - title: Order price
          type: object
          required:
            - type
          properties:
            type:
              type: string
              const: order_price
            minimum:
              $ref: '#/components/schemas/Money'
            maximum:
              $ref: '#/components/schemas/Money'
        - title: Order weight
          type: object
          required:
            - type
          properties:
            type:
              type: string
              const: order_weight
            minimum:
              $ref: '#/components/schemas/Weight'
            maximum:
              $ref: '#/components/schemas/Weight'
    TransitTime:
      type: object
      description: >-
        The delivery estimate shown with the rate. `max_days` must be greater
        than or equal to `min_days`.
      properties:
        min_days:
          type: integer
          minimum: 0
        max_days:
          type: integer
          minimum: 0
    DiscountValue:
      description: >-
        How much the discount takes off. Always unsigned, because a discount
        only ever reduces the price.
      oneOf:
        - title: Percentage
          type: object
          required:
            - unit
            - value
          properties:
            unit:
              type: string
              const: percentage
            value:
              type: number
              exclusiveMinimum: 0
              maximum: 100
              description: |
                Percent off. Writes accept integers. Offers authored in the
                ABConvert admin can read back decimals.
            max_discount_amount:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: |
                The most this discount can take off the order. Percentage
                discounts only. A fixed amount is already absolute, so
                sending a cap with one returns 422
                `max_discount_amount_not_allowed`.
        - title: Fixed amount
          type: object
          required:
            - unit
            - amount
          properties:
            unit:
              type: string
              const: fixed
            amount:
              $ref: '#/components/schemas/Money'
            max_discount_amount:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: |
                Refused here. A fixed amount is already absolute, so sending
                a cap with one returns 422 `max_discount_amount_not_allowed`.
    DiscountScope:
      description: Which products the discount applies to. Omitted = all products.
      oneOf:
        - title: All products
          type: object
          required:
            - type
          properties:
            type:
              type: string
              const: all_products
        - title: Specific products
          type: object
          required:
            - type
            - product_ids
          properties:
            type:
              type: string
              const: specific_products
            product_ids:
              type: array
              minItems: 1
              maxItems: 250
              items:
                allOf:
                  - $ref: '#/components/schemas/ShopifyGid'
                example: gid://shopify/Product/8123456789
        - title: Specific collections
          type: object
          required:
            - type
            - collection_ids
          properties:
            type:
              type: string
              const: specific_collections
            collection_ids:
              type: array
              minItems: 1
              maxItems: 250
              items:
                allOf:
                  - $ref: '#/components/schemas/ShopifyGid'
                example: gid://shopify/Collection/308896989
    DiscountCondition:
      oneOf:
        - title: Order price
          type: object
          required:
            - type
            - minimum
          properties:
            type:
              type: string
              const: order_price
            minimum:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: Minimum order subtotal to qualify.
        - title: Item quantity
          type: object
          required:
            - type
            - minimum
          properties:
            type:
              type: string
              const: quantity
            minimum:
              type: integer
              minimum: 1
              description: Minimum item quantity to qualify.
    ShippingDiscountScope:
      description: Which shipping rates the discount applies to. Omitted = all rates.
      oneOf:
        - title: All rates
          type: object
          required:
            - type
          properties:
            type:
              type: string
              const: all_rates
        - title: Specific rates
          type: object
          required:
            - type
            - rate_names
          properties:
            type:
              type: string
              const: specific_rates
            rate_names:
              type: array
              minItems: 1
              description: |
                The discount matches rates by name. Scoped rates have no
                stable GID, so the name is the only thing to match on.
              items:
                type: string
                minLength: 1
                example: Express
    VolumeTier:
      type: object
      required:
        - threshold
        - value
      properties:
        threshold:
          type: integer
          minimum: 1
          description: Minimum item count for this tier.
        value:
          $ref: '#/components/schemas/DiscountValue'
    ThresholdTier:
      type: object
      required:
        - threshold
        - value
      properties:
        threshold:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Minimum order subtotal for this tier.
        value:
          $ref: '#/components/schemas/DiscountValue'
    Weight:
      type: object
      required:
        - value
        - unit
      properties:
        value:
          type: number
          minimum: 0
          example: 2.5
        unit:
          type: string
          enum:
            - g
            - kg
            - oz
            - lb
          example: kg
  headers:
    IdempotencyReplayed:
      description: >
        `true` when this response is a replay of an earlier request carrying

        the same `Idempotency-Key`. Absent otherwise, and never sent by

        [`POST
        /experiments/{id}/exports`](/api-reference/exports/create-an-export-job),

        which re-reads the export job on a

        replay instead of repeating the original response.
      schema:
        type: string
        const: 'true'
    XRateLimitLimit:
      description: |
        How many requests this token may make in the current 60-second window.
        Reads, writes, and result queries each have a separate budget, so this
        is the budget for the class this request falls in.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests left in the current window.
      schema:
        type: integer
    XRateLimitReset:
      description: Unix timestamp when the window resets.
      schema:
        type: integer
  responses:
    BadRequest:
      description: |
        `invalid_request_error`. The request never reached the test, so nothing
        changed. Fix the request and send it again.

        - `malformed_json`: the body isn't valid JSON.
        - `unknown_parameter`: `param` names a field this endpoint doesn't
          accept. Remove it.
        - `invalid_cursor`: the cursor is stale or invalid. Start the list
          again from the first page.
        - `invalid_include`: `include` accepts `results_summary` only.
        - `invalid_idempotency_key`: send a key of 1 to 255 characters, or omit
          the header.
        - `invalid_request`: `param` names the query parameter to fix.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: |
        `authentication_error`. Send a valid token as
        `Authorization: Bearer <token>`.

        - `missing_token`: no bearer token on the request.
        - `invalid_token`: the token is malformed, does not exist, or names a
          shop ABConvert can no longer act for.
        - `token_revoked`: someone revoked this token in the ABConvert admin.
          Create a new one.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >
        `permission_error`. The shop or the token isn't allowed to do this, so

        nothing changed.


        | Code | What to do | Where it applies |

        |---|---|---|

        | `insufficient_scope` | Use a token with the scope named in
        `details.required_scope`. | Any request |

        | `api_access_disabled` | API access is turned off for this shop.
        Contact support. | Any request |

        | `subscription_inactive` | Renew the shop's ABConvert subscription,
        then retry. | `start` and `resume` |

        | `billing_cap_reached` | Raise the shop's usage cap for the period,
        then retry. | `start` and `resume` |

        | `feature_not_in_plan` | Upgrade to the plan named in `message`.
        `param` names the feature: `price`, `offer`, `multi_market`, or
        `checkout_blocks`. | Create, update, `preview`, `start`, `resume` |

        | `carrier_service_required` | Give the shop carrier-calculated
        shipping. A Shopify plan with the feature, an existing carrier service,
        or annual ABConvert billing each satisfy it. | Shipping tests |


        `pause`, `end`, and `archive` never return entitlement errors. See

        [Feature availability](/api-reference/overview#feature-availability).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: |
        `conflict_error`. One of these codes:

        - `invalid_status_transition`: `message` names the current status and
          the actions it allows, and `details` carries the same facts as
          data: `current_status` and `allowed_actions`.
        - `locked_field`: you tried to change a field that locks when a test
          launches. After launch you can still change `name`, `hypothesis`,
          `primary_metric`, a test group's `name` and `split`, and
          `schedule.end_at`; to change anything else, create a new test.
        - `idempotency_key_in_use`: another request already claimed this key.
          Use a new key for a new request, or retry with the same key in 90
          seconds if the first one might still be running. See
          [Idempotency](/api-reference/overview#idempotency).
        - `checks_pending`: ABConvert is still validating this test. Retry in
          about a minute. `archive` stays available meanwhile.
        - `editor_owned_content`: edit this test group in the ABConvert admin.
          It holds content the API can read but not rewrite, such as a checkout
          customization's display conditions or a checkout block, so the API
          refuses the change rather than erasing it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationFailed:
      description: >
        `validation_error`. On a request body, the `findings` array lists

        every blocking finding. On a query string, the error carries `param`

        naming the parameter, and no `findings`.


        `preview_unsupported` is the one code here that judges the test rather

        than the request: see

        [`POST
        /experiments/{id}/preview`](/api-reference/lifecycle/preview-a-test). It
        carries

        neither `findings` nor `param`, because nothing about the request is

        wrong.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: |
        `rate_limit_error`, code `rate_limited`. You sent more requests than
        your budget allows. Wait the number of seconds in `Retry-After`, then
        retry.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until the window resets.
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnexpectedError:
      description: |
        A status this operation doesn't enumerate. The body is the standard
        error shape.

        - 413 `request_too_large`: the body is over 10 MB. Send less.
        - 500 `internal_error`: something went wrong on our side. Retry. If it
          keeps failing, send support the `request_id`.
        - 503: the request was refused before it ran, so nothing on the shop
          changed. Retry it, with the same `Idempotency-Key` if you sent one.
          The code names which safeguard was unavailable:
          `rate_limit_unavailable`, `audit_unavailable`,
          `idempotency_unavailable`, or `internal_error`.
        - 504 `internal_error`: the write timed out and nothing changed. Retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Bearer tokens created in the ABConvert admin under
        Settings → MCP & API Access. Scopes:
        `read_experiments` and `write_experiments` (write implies read; the
        default is read). See
        [Authentication](/api-reference/authentication) for which scope each
        request needs.

````