> ## 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.

# Download an export file

> Streams the finished export as CSV, one row per attributed order.
`Experiment ID` carries the test's numeric ID and `Test Group` carries
the test group's index, so a row joins back to
`GET /v1/experiments/{id}`. Test group names live on the test, not in
the file.

A completed job's `url` is the complete download link. Fetch it as
given: its `expires` and `signature` parameters authorize the
request.

**The link is the credential.** This is the one endpoint that takes
no bearer token, so treat the whole URL as a secret. It stops working
7 days after the job completed.

Anything wrong with the request answers the same 404
`export_not_found`: a bad signature, an expired link, an unknown
job, and an unfinished job all read alike, so an unauthenticated
caller learns nothing about which jobs exist.




## OpenAPI

````yaml /api-reference/openapi.yaml get /exports/{id}/download
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:
  /exports/{id}/download:
    get:
      tags:
        - Exports
      summary: Download an export file
      description: |
        Streams the finished export as CSV, one row per attributed order.
        `Experiment ID` carries the test's numeric ID and `Test Group` carries
        the test group's index, so a row joins back to
        `GET /v1/experiments/{id}`. Test group names live on the test, not in
        the file.

        A completed job's `url` is the complete download link. Fetch it as
        given: its `expires` and `signature` parameters authorize the
        request.

        **The link is the credential.** This is the one endpoint that takes
        no bearer token, so treat the whole URL as a secret. It stops working
        7 days after the job completed.

        Anything wrong with the request answers the same 404
        `export_not_found`: a bad signature, an expired link, an unknown
        job, and an unfinished job all read alike, so an unauthenticated
        caller learns nothing about which jobs exist.
      operationId: downloadExport
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Export job ID (opaque string).
        - name: expires
          in: query
          required: true
          schema:
            type: integer
          description: Unix seconds, from the job's `url`.
        - name: signature
          in: query
          required: true
          schema:
            type: string
          description: Signature over the id and expiry, from the job's `url`.
      responses:
        '200':
          description: |
            The export, as `text/csv`. Sent as an attachment named
            `abconvert-orders-{experiment_id}.csv`.
          content:
            text/csv:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'
        default:
          $ref: '#/components/responses/UnexpectedError'
      security: []
components:
  responses:
    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'
    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'
  schemas:
    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
  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.

````