> ## 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 an export job

> Starts an export of the test's order-level data for a date range. The
job runs asynchronously, so poll
[`GET /exports/{id}`](/api-reference/exports/retrieve-an-export-job) until it
completes, then fetch the file from the `url` that job carries. The
cookbook's
[order export](https://github.com/ABConvert/abconvert-cookbook/tree/main/examples/order-export)
runs the whole flow, download and analysis included.

Each row carries the same columns as the ABConvert admin's order
export. That column schema is in beta.

The export is cut the same way the Analytics dashboard cuts its
numbers, so the two reconcile: your configured outlier filter is
applied automatically, and `sample_basis` chooses the same
denominator the dashboard offers. The export isn't capped, so a wide
date range returns every attributed order.

A test that hasn't started yet returns 422 `export_not_available`:
nothing exists to export until it has run.

A `read_experiments` token may call this: it downloads data and
changes nothing about the test. It still spends write budget and is
recorded in the audit log, because it exports order-level data in
bulk.




## OpenAPI

````yaml /api-reference/openapi.yaml post /experiments/{id}/exports
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/{id}/exports:
    post:
      tags:
        - Exports
      summary: Create an export job
      description: >
        Starts an export of the test's order-level data for a date range. The

        job runs asynchronously, so poll

        [`GET /exports/{id}`](/api-reference/exports/retrieve-an-export-job)
        until it

        completes, then fetch the file from the `url` that job carries. The

        cookbook's

        [order
        export](https://github.com/ABConvert/abconvert-cookbook/tree/main/examples/order-export)

        runs the whole flow, download and analysis included.


        Each row carries the same columns as the ABConvert admin's order

        export. That column schema is in beta.


        The export is cut the same way the Analytics dashboard cuts its

        numbers, so the two reconcile: your configured outlier filter is

        applied automatically, and `sample_basis` chooses the same

        denominator the dashboard offers. The export isn't capped, so a wide

        date range returns every attributed order.


        A test that hasn't started yet returns 422 `export_not_available`:

        nothing exists to export until it has run.


        A `read_experiments` token may call this: it downloads data and

        changes nothing about the test. It still spends write budget and is

        recorded in the audit log, because it exports order-level data in

        bulk.
      operationId: createExperimentExport
      parameters:
        - $ref: '#/components/parameters/ExperimentId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - date_range
              description: |
                Beta. More filters may be added later. An unknown field at the
                top level is refused with 400 `unknown_parameter` rather than
                ignored; an unknown key inside `date_range` is refused with
                422 `invalid_date_range`.
              properties:
                date_range:
                  type: object
                  additionalProperties: false
                  required:
                    - gte
                    - lte
                  description: |
                    Both bounds are required and inclusive, and are calendar
                    days in your store's timezone.

                    The window is narrowed to the days that can hold data: it
                    starts no earlier than the day the test started, and ends
                    no later than today. A window that can hold nothing at
                    all returns 422 `date_range_out_of_bounds`.
                  properties:
                    gte:
                      type: string
                      format: date
                      example: '2026-08-01'
                    lte:
                      type: string
                      format: date
                      example: '2026-08-15'
                sample_basis:
                  type: string
                  enum:
                    - assignment
                    - exposure
                  default: assignment
                  description: |
                    Which visitors count. `assignment` includes every visitor
                    put in a test group; `exposure` only those the test
                    actually reached.
      responses:
        '202':
          description: The created job.
          headers:
            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/ExportJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        default:
          $ref: '#/components/responses/UnexpectedError'
      security:
        - bearerAuth: []
components:
  parameters:
    ExperimentId:
      name: id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[0-9]+$
        example: '3021'
      description: The test's numeric ID as a string, for example `"3021"`.
    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).
  headers:
    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
  schemas:
    ExportJob:
      type: object
      required:
        - object
        - id
        - status
      properties:
        object:
          type: string
          const: export
        id:
          type: string
          description: Treat as opaque. Never parse it.
          example: export_9f2c1a7b3d5e4f60a1b2c3d4e5f60718
        experiment_id:
          type: string
          example: '3021'
        status:
          type: string
          enum:
            - pending
            - processing
            - completed
            - failed
          example: completed
        url:
          type:
            - string
            - 'null'
          format: uri
          description: |
            The signed download link, ready to fetch as given. Set while the
            job is `completed` and `expires_at` is still in the future; null
            before the job finishes, and null again once the link expires,
            while `status` stays `completed`. Branch on this field rather
            than on `status`.

            The link is signed, so fetching it needs no bearer token. You can
            hand the whole URL to a browser or a download tool. Anyone who has
            the URL can download the file until `expires_at`.
          example: >-
            https://app.abconvert.io/v1/exports/export_9f2c1a7b3d5e4f60a1b2c3d4e5f60718/download?expires=1787000000&signature=3f8c1d0b6a94e27d5c1b8e40f7a2d9c6b3e15f8a0d47c92e6b1a3f5d8c07e246
        expires_at:
          type:
            - string
            - 'null'
          format: date-time
        failure_reason:
          type:
            - string
            - 'null'
          description: Why the job failed. Set only when `status` is `failed`.
        created_at:
          type: string
          format: date-time
    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'
    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
  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'
    NotFound:
      description: |
        `not_found_error`. No resource with that ID belongs to this shop. Check
        the ID, and check that the token belongs to the shop that owns the
        resource: a token reaches one shop only.
      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.

````