openapi: 3.1.0
info:
  title: Bulk Operations API
  description: |-
    API for performing bulk operations for various resources

    ### Resource identifier format

    Resource identifiers (the `id` field and related global-identifier fields) are returned as JSON
    integers by default. To receive them as strings instead, send the
    `X-Loop-Global-Identifier-Type: string` request header.
  version: "v1"
tags:
  - name: Bulk Operations
    description: Operations related to managing resources in bulk
servers:
  - url: https://api.loopreturns.com/api/2026-10
    description: 2026-10 (Stable)
  - url: https://api.loopreturns.com/api/2026-07
    description: 2026-07
  - url: https://api.loopreturns.com/api/unstable
    description: Unstable (Preview)
paths:
  /bulk-operations:
    post:
      summary: Create Bulk Operation
      description: |
        Create a new bulk operation.
      operationId: createBulkOperation
      tags:
        - Bulk Operations
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CreateBulkOperationRequest"
      responses:
        "201":
          description: "Success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  bulk_operation:
                    $ref: "#/components/schemas/BulkOperationResponse"
        "401":
          description: "Unauthorized"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnauthorizedResponse"
        "422":
          description: "Unprocessable Entity"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationErrorResponse"
      security:
        - write: []
    get:
      summary: List Bulk Operations
      description: Retrieve a list of bulk operations. The list can be sorted and paginated using the parameters below.
      operationId: listBulkOperations
      tags:
        - Bulk Operations
      parameters:
        - name: limit
          in: query
          description: "The maximum number of bulk operations returned."
          required: false
          example: 20
          schema:
            type: integer
            maximum: 250
            default: 50
        - name: sort_order
          in: query
          description: "The sort order for the results."
          required: false
          schema:
            type: string
            enum:
              - asc
              - desc
            default: asc
        - name: cursor
          in: query
          description: "The cursor to return the next page of results."
          required: false
          schema:
            type: string
      responses:
        "200":
          description: "Success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  bulk_operations:
                    type: array
                    items:
                      $ref: "#/components/schemas/BulkOperationResponse"
                    description: An array of bulk operations.
                  next_page_url:
                    type: [string, "null"]
                  previous_page_url:
                    type: [string, "null"]
        "401":
          description: "Unauthorized"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnauthorizedResponse"
      security:
        - read: []
  /bulk-operations/{id}:
    get:
      summary: Get Bulk Operation
      description: Get the details about a specific bulk operation.
      operationId: getBulkOperation
      tags:
        - Bulk Operations
      parameters:
        - in: path
          name: id
          schema:
            type: integer
          required: true
          description: The unique identifier for the bulk operation, created by Loop.
      responses:
        "200":
          description: "Success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  bulk_operation:
                    $ref: "#/components/schemas/BulkOperationResponse"
        "401":
          description: "Unauthorized"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnauthorizedResponse"
        "404":
          description: "Not Found"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotFoundResponse"
      security:
        - read: []
  /bulk-operations/{id}/jobs:
    get:
      summary: List Bulk Operation Jobs
      description: |
        Retrieve a list of bulk operation jobs.
        The list can be sorted and paginated using the parameters below.
      operationId: listBulkOperationJobs
      tags:
        - Bulk Operations
      parameters:
        - in: path
          name: id
          schema:
            type: integer
          required: true
          description: The unique identifier for the bulk operation, created by Loop.
        - name: limit
          in: query
          description: The maximum number of bulk operation jobs returned.
          required: false
          schema:
            type: integer
            maximum: 250
            default: 50
        - name: sort_order
          in: query
          description: "The sort order for the results."
          required: false
          schema:
            type: string
            enum:
              - asc
              - desc
            default: asc
        - name: cursor
          in: query
          description: "The cursor to return the next page of results."
          required: false
          schema:
            type: string
      responses:
        "200":
          description: "Success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  bulk_operation_jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/BulkOperationJobResponse"
                    description: An array of bulk operation jobs.
                  next_page_url:
                    type: [string, "null"]
                  previous_page_url:
                    type: [string, "null"]
        "401":
          description: "Unauthorized"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnauthorizedResponse"
      security:
        - read: []
components:
  schemas:
    ValidationErrorResponse:
      type: object
      description: >-
        Returned when the request fails validation. Uses Laravel's standard
        validation error shape.
      properties:
        message:
          type: string
          examples:
            - "The file field is required."
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          examples:
            - file:
                - "The file field is required."
    CreateBulkOperationRequest:
      type: object
      properties:
        file:
          type: string
          format: binary
          description: |
            The JSONL file containing the data for the bulk operations.

            Each line contains an individual operation, formatted as follows:

            `{"type": "{job type}", "parameters": {}, "payload": {}}`

            The following job types are supported:
            - [`order_create`](/api-reference/latest/orders/create-order)
            - [`order_update`](/api-reference/latest/orders/update-order)
            - [`order_upsert`](/api-reference/latest/orders/upsert-order)
            - [`order_delete`](/api-reference/latest/orders/delete-order)
            - [`product_create`](/api-reference/latest/products/create-product)
            - [`product_update`](/api-reference/latest/products/update-product)
            - [`product_upsert`](/api-reference/latest/products/upsert-product)
            - [`product_delete`](/api-reference/latest/products/delete-product)
            - [`product_variant_create`](/api-reference/latest/products/create-product-variant)
            - [`product_variant_update`](/api-reference/latest/products/update-product-variant)
            - [`product_variant_upsert`](/api-reference/latest/products/upsert-product-variant)
            - [`product_variant_delete`](/api-reference/latest/products/delete-product-variant)
            - [`collection_create`](/api-reference/latest/collections/create-collection)
            - [`collection_update`](/api-reference/latest/collections/update-collection)
            - [`collection_upsert`](/api-reference/latest/collections/upsert-collection)
            - [`collection_delete`](/api-reference/latest/collections/delete-collection)
            - [`inventory_upsert`](/api-reference/latest/inventories/upsert-inventory)
            - [`inventory_delete`](/api-reference/latest/inventories/delete-inventory)

            #### Addressing records in job parameters

            The following delete and upsert job types accept an optional `addressing`
            parameter that controls how the target record is identified:

            - `global` (default) — use Loop global IDs in `parameters`
            - `external` — use your platform's external IDs in `parameters`

            When `addressing` is omitted, these jobs default to `global`. An invalid
            `addressing` value causes the job to fail with a validation error. If the
            external ID does not resolve to an existing record, the job fails as not found.

            For each supported job type, Global is the default `parameters` shape.
            External applies when `addressing` is `"external"`:

            - `order_delete`, `product_delete`, `collection_delete`
              - Global: `id`
              - External: `externalId`
            - `product_variant_delete`
              - Global: `id`, `productId`
              - External: `productExternalId`, `externalId`
            - `product_variant_upsert`
              - Global: `productId`
              - External: `productExternalId`
            - `inventory_upsert`, `inventory_delete`
              - Global: `productVariantGlobalId`, `locationGlobalId`
              - External: `variantExternalId`, `locationExternalId`

            `order_upsert`, `product_upsert`, and `collection_upsert` always match by
            `external_id` in the `payload` and do not use an `addressing` parameter.
            Create and update jobs use only global addressing (for example, `id` or
            `productId` in `parameters`).

            For `inventory_upsert` and `inventory_delete` with `addressing` set to
            `"external"`, if more than one product variant in the shop matches
            `variantExternalId`, the job fails with a conflict error.

            Payload details can be found in the documentation for each job, linked above.

            #### Example job lines

            Examples below are pretty-printed for readability. In the JSONL file, each
            job must be a single line with no embedded newlines.

            **Update a product by Loop global ID (default addressing)**

            ```json
            {
              "type": "product_update",
              "parameters": {
                "id": 1234567890
              },
              "payload": {
                "external_id": "product-example-id",
                "sku": "LOOP-12345",
                "name": "Example",
                "weight_grams": 500,
                "barcode": "Updated-Barcode",
                "description": "An example product",
                "type": "Example Product Type",
                "vendor": "Internal",
                "price": {
                  "currency_code": "USD",
                  "amount": 100
                },
                "created_at": "2023-08-10T15:00:00+00:00",
                "updated_at": "2023-08-12T15:00:00+00:00",
                "source": "SFCC",
                "sales_channel": "Online Store",
                "options": [
                  {
                    "position": 1,
                    "name": "Color",
                    "values": ["red", "green", "blue"]
                  },
                  {
                    "position": 2,
                    "name": "Condition",
                    "values": ["new", "used", "refurbished"]
                  }
                ],
                "images": [
                  "https://example.com/image.jpg",
                  "https://example.com/image.png"
                ],
                "tags": ["Socks", "Sandals", "Sunglasses"]
              }
            }
            ```

            **Delete an order by external ID**

            ```json
            {
              "type": "order_delete",
              "parameters": {
                "addressing": "external",
                "externalId": "shopify-order-1001"
              },
              "payload": {}
            }
            ```

            **Upsert a product variant by external product ID**

            ```json
            {
              "type": "product_variant_upsert",
              "parameters": {
                "addressing": "external",
                "productExternalId": "prod-001"
              },
              "payload": {
                "external_id": "var-red-m",
                "name": "Red / Medium"
              }
            }
            ```

            **Upsert inventory by external IDs**

            ```json
            {
              "type": "inventory_upsert",
              "parameters": {
                "addressing": "external",
                "variantExternalId": "4006381333931",
                "locationExternalId": "main-warehouse"
              },
              "payload": {
                "available_count": 42
              }
            }
            ```

            **Delete inventory by external IDs**

            ```json
            {
              "type": "inventory_delete",
              "parameters": {
                "addressing": "external",
                "variantExternalId": "4006381333931",
                "locationExternalId": "main-warehouse"
              },
              "payload": {}
            }
            ```

            Lines must be separated with `\n`.

            There is no limit to the number of lines that can be included in a file,
            but bulk operation files are limited to 2 MB in size.
      required:
        - file
    BulkOperationResponse:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: The unique identifier for the bulk operation, created by Loop.
          examples:
            - 641174584320376089
        status:
          type: ["string", "null"]
          description: The status of the bulk operation.
          enum:
            - pending
            - processed_with_failures
            - processed_successful
        pending_count:
          type: integer
          format: int32
          description: The number of pending jobs.
          examples:
            - 1
        failed_count:
          type: integer
          format: int32
          description: The number of failed jobs.
          examples:
            - 0
        success_count:
          type: integer
          format: int32
          description: The number of successful jobs.
          examples:
            - 0
        created_at:
          type: string
          format: date-time
          description: "The date and time at which the bulk operation was created."
          examples:
            - "2023-04-25T13:25:00-05:00"
        updated_at:
          type: string
          format: date-time
          examples:
            - "2023-04-27T13:25:00-05:00"
          description: "The date and time at which the bulk operation was updated."
    BulkOperationJobResponse:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: The unique identifier for the bulk operation job, created by Loop.
        type:
          type: ["string", "null"]
          description: The bulk operation job type.
          enum:
            - order_create
            - order_update
            - order_upsert
            - order_delete
            - product_create
            - product_update
            - product_upsert
            - product_delete
            - product_variant_create
            - product_variant_update
            - product_variant_upsert
            - product_variant_delete
            - collection_create
            - collection_update
            - collection_upsert
            - collection_delete
            - inventory_upsert
            - inventory_delete
        status:
          type: ["string", "null"]
          description: The status of the bulk operation job.
          enum:
            - pending
            - failed
            - success
        row_number:
          type: integer
          format: int32
          description: The row number in the JSONL file.
          examples:
            - 42
        result:
          type: [string, "null"]
          format: json
          description: The response of the bulk operation job. `null` until the job has run.
          examples:
            - '{"product": {"id": 810772, "external_id": "example-id", "sku": "SHOES-US44-M-Brwn"}}'
        created_at:
          type: string
          format: date-time
          examples:
            - "2023-04-25T13:25:00-05:00"
          description: "The date and time at which the bulk operation was created."
        updated_at:
          type: string
          format: date-time
          examples:
            - "2023-04-25T13:25:00-05:00"
          description: "The date and time at which the bulk operation was updated."
    UnauthorizedResponse:
      type: object
      required:
        - errors
      properties:
        errors:
          type: string
          examples:
            - "Unauthorized."
    NotFoundResponse:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          examples:
            - "Resource not found."
  securitySchemes:
    read:
      type: apiKey
      in: header
      name: X-Authorization
      description: 'API Scope: "Bulk Operations (read)"'
      x-scope: Bulk Operations (Read)
    write:
      type: apiKey
      in: header
      name: X-Authorization
      description: 'API Scope: "Bulk Operations (write)"'
      x-scope: Bulk Operations (Write)
