openapi: 3.1.0
info:
  title: Checkout+ API
  description: |
    The Checkout+ API enables platform-agnostic order protection for e-commerce stores.
    It analyzes cart data, determines eligibility, calculates pricing, and returns
    charge instructions that tell your integration how to apply the protection fee.

    All monetary amounts are in **minor currency units** (e.g., cents for USD).
    Currency codes follow ISO 4217.
  version: 'v1'
servers:
  - url: https://api.loopreturns.com/storefront/v1
    description: Production
tags:
  - name: Checkout+
paths:
  /analyze:
    post:
      summary: Analyze Cart
      operationId: analyzeCart
      description: |
        Analyze a cart to determine order protection eligibility, calculate pricing,
        and receive charge instructions.

        This is the primary endpoint for Checkout+ integrations. Call it when the
        customer's cart changes or when they reach checkout, and use the response
        to present the protection offer and apply the charge.

        **Idempotency:** You can link a request to an existing analysis in two
        ways: pass the `sessionId` returned by a previous response, or provide
        a top-level `cartId` (matched per merchant). Either approach updates the
        existing analysis rather than creating a new one.

        **Identity:** The merchant is identified by the API key on the request
        — see [Authentication](https://docs.loopreturns.com/api-reference/authentication).
      tags:
        - Checkout+
      security:
        - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalyzeRequest'
            example:
              platform: woocommerce
              region: US
              cartId: cart_8c2f9e7d1a4b
              cart:
                lines:
                  - id: line_001
                    productId: prod_67890
                    quantity: 2
                    unitPrice:
                      amount: 2500
                      currencyCode: USD
                    totalPrice:
                      amount: 5000
                      currencyCode: USD
                    originalTotalPrice:
                      amount: 5000
                      currencyCode: USD
                    title: Classic T-Shirt
                itemCount: 2
                subtotalAmount: 5000
                totalDiscountAmount: 0
                totalTaxAmount: 400
                discountedSubtotalAmount: 5000
                totalAmount: 5400
                currencyCode: USD
      responses:
        '200':
          description: Cart analysis completed successfully.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyzeResponse'
              examples:
                eligible_combined_coverage:
                  summary: Eligible — return coverage and shipping protection
                  value:
                    sessionId: lop_a1b2c3d4e5f6g7h8i9j0
                    eligible: true
                    reason: eligible
                    mode:
                      - returnCoverage
                      - shippingProtection
                    chargeInstructions:
                      amount: 498
                      currencyCode: USD
                      method: fee
                    metadata:
                      priceBreakdown:
                        - type: base
                          amount: 298
                          label: Return coverage
                        - type: base
                          amount: 200
                          label: Shipping protection
                      pricingSubtotalUsed: 5000
                      experimentCohort: null
                      sessionCreated: true
                      analysisCount: 1
                eligible_shopify:
                  summary: Eligible — Shopify (product line item)
                  value:
                    sessionId: lop_a1b2c3d4e5f6g7h8i9j0
                    eligible: true
                    reason: eligible
                    mode:
                      - returnCoverage
                      - shippingProtection
                    chargeInstructions:
                      amount: 498
                      currencyCode: USD
                      method: product_line_item
                      product:
                        id: '8001234567890'
                        handle: loop-order-protection
                      lineItemProperties:
                        _mode: returnCoverage,shippingProtection
                    cartAttributes:
                      _loop_session_id: lop_a1b2c3d4e5f6g7h8i9j0
                    metadata:
                      priceBreakdown:
                        - type: base
                          amount: 298
                          label: Return coverage
                        - type: base
                          amount: 200
                          label: Shipping protection
                      pricingSubtotalUsed: 5000
                      experimentCohort: null
                      sessionCreated: true
                      analysisCount: 1
                ineligible:
                  summary: Ineligible cart
                  value:
                    sessionId: lop_a1b2c3d4e5f6g7h8i9j0
                    eligible: false
                    reason: cart_total_too_low
                    chargeInstructions: null
                    metadata:
                      priceBreakdown: []
                      experimentCohort: null
                      sessionCreated: true
                      analysisCount: 1
        '400':
          description: Invalid request — malformed JSON, missing required fields, or schema validation failure.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  summary: Validation error
                  value:
                    error:
                      code: validation_error
                      message: Request validation failed
                      details:
                        cart.currencyCode: Required
                        cart.totalAmount: Expected number, received string
                missing_field:
                  summary: Missing required field
                  value:
                    error:
                      code: invalid_request
                      message: Malformed JSON or missing required fields
                body_too_large:
                  summary: Request body exceeds the 1 MB ceiling
                  value:
                    error:
                      code: invalid_request
                      message: Request body too large
        '401':
          description: >
            Missing or invalid authentication — see
            [Authentication](https://docs.loopreturns.com/api-reference/authentication).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: unauthorized
                  message: Missing or invalid API key
        '404':
          description: |
            The supplied `sessionId` is well-formed but does not exist for
            this merchant (typically because it was created in another
            environment, has been evicted, or was never created under this
            API key). Omit `sessionId` to start a new session.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: session_not_found
                  message: sessionId does not exist
        '405':
          description: Wrong HTTP method for this endpoint. `/v1/analyze` only accepts `POST`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: method_not_allowed
                  message: Method not allowed. Use POST.
        '429':
          description: Per-merchant rate limit exceeded.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: rate_limited
                  message: Rate limit exceeded. Retry after the indicated period.
        '500':
          description: Unexpected server error.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: internal_error
                  message: An unexpected error occurred
        '503':
          description: |
            A required upstream dependency (typically the merchant
            configuration resolver) is temporarily unavailable. The
            request did not reach business logic — safe to retry with
            backoff.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: service_unavailable
                  message: Merchant configuration is temporarily unavailable

  /session/{sessionId}:
    get:
      summary: Get Offer Session
      operationId: getOfferSession
      description: |
        Fetch a previously created offer session by id. The response is
        the same shape as `POST /v1/analyze` minus the request-driven
        `metadata.sessionCreated` flag, plus `createdAt` and `updatedAt`
        timestamps.

        Use this endpoint to inspect an existing session — for example
        from a debug surface, an admin tool, or a follow-up worker that
        only has the `sessionId` and not the cart payload.

        Sessions are merchant-scoped: a session created under one
        merchant API key is not visible to another. A 404 with code
        `session_not_found` is returned both when the id does not exist
        and when it belongs to a different merchant.

        Responses are cached privately for 45 seconds
        (`Cache-Control: private, max-age=45`).
      tags:
        - Checkout+
      security:
        - api_key: []
      parameters:
        - name: sessionId
          in: path
          required: true
          description: Offer session identifier returned by a previous `/analyze` call.
          schema:
            type: string
            pattern: '^lop_[A-Za-z0-9]{20}$'
            example: lop_a1b2c3d4e5f6g7h8i9j0
      responses:
        '200':
          description: Offer session retrieved.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            Cache-Control:
              description: >
                Always `private, max-age=45`. The response is per-merchant
                and may be cached briefly by the integrator.
              schema:
                type: string
                example: 'private, max-age=45'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
              example:
                sessionId: lop_a1b2c3d4e5f6g7h8i9j0
                eligible: true
                reason: eligible
                mode:
                  - returnCoverage
                  - shippingProtection
                chargeInstructions:
                  amount: 498
                  currencyCode: USD
                  method: fee
                createdAt: '2026-04-23T13:25:00+00:00'
                updatedAt: '2026-04-23T13:27:14+00:00'
                metadata:
                  priceBreakdown:
                    - type: base
                      amount: 298
                      label: Return coverage
                    - type: base
                      amount: 200
                      label: Shipping protection
                  pricingSubtotalUsed: 5000
                  experimentCohort: null
                  analysisCount: 3
        '401':
          description: >
            Missing or invalid authentication — see
            [Authentication](https://docs.loopreturns.com/api-reference/authentication).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: unauthorized
                  message: Missing or invalid API key
        '404':
          description: |
            Session not found — either the id does not exist or it
            belongs to a different merchant.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: session_not_found
                  message: Session not found
        '405':
          description: Wrong HTTP method for this endpoint. `/v1/session/{sessionId}` only accepts `GET`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: method_not_allowed
                  message: Method not allowed. Use GET.
        '429':
          description: Per-merchant rate limit exceeded.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: rate_limited
                  message: Rate limit exceeded. Retry after the indicated period.
        '500':
          description: Unexpected server error.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: internal_error
                  message: An unexpected error occurred
        '503':
          description: |
            A required upstream dependency (typically the merchant
            configuration resolver) is temporarily unavailable. The
            request did not reach business logic — safe to retry with
            backoff.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: service_unavailable
                  message: Merchant configuration is temporarily unavailable

components:
  schemas:
    # ---------------------------------------------------------------------------
    # Request
    # ---------------------------------------------------------------------------
    AnalyzeRequest:
      type: object
      required:
        - platform
        - region
        - cart
      properties:
        platform:
          type: string
          description: |
            Your e-commerce platform. Recognized values are `shopify`,
            `woocommerce`, `bigcommerce`, and `custom`; any other string is
            accepted for unsupported platforms and stored as-is.
          example: woocommerce
        region:
          description: |
            Geographic region for eligibility evaluation. Represents the
            customer or session geography (geolocation, customer profile,
            shipping address, or store configuration).

            Accepts either:
            - An ISO 3166-1 alpha-2 country code (e.g. `"US"`).
            - An ISO 3166-2 region code (e.g. `"US-OH"`).
            - A structured `{ country, subdivision? }` object that the API
              normalizes to the same canonical uppercase string.
          oneOf:
            - type: string
              example: US
            - type: object
              required:
                - country
              properties:
                country:
                  type: string
                  example: US
                subdivision:
                  type: string
                  example: OH
          example: US
        experimentationConsent:
          type: boolean
          description: |
            Opt in to A/B test cohort assignment. When `true`, the server may
            assign the session to an experiment cohort and return it in
            `metadata.experimentCohort`. When `false` or omitted, no cohort is
            assigned and `metadata.experimentCohort` is `null`.
          default: false
        debug:
          type: boolean
          description: |
            When `true`, the response includes verbose diagnostics in the
            `debug` field. Use during development and integration testing
            only — there is no rate-limit or payload-size advantage to
            enabling it in production.
          default: false
        sessionId:
          type: string
          description: |
            Session ID from a previous `/analyze` response. Pass this to
            update an existing analysis instead of creating a new one.

            Takes precedence over `cartId` for session lookup. If the value
            does not match the expected format, the request fails with
            `400 invalid_request`. If the format is valid but no such
            session exists for this merchant, the request fails with
            `404 session_not_found` — omit the field to start a new
            session.
          pattern: '^lop_[A-Za-z0-9]{20}$'
          example: lop_a1b2c3d4e5f6g7h8i9j0
        cartId:
          type: string
          description: |
            Stable cart identifier from your platform. When provided (and
            no `sessionId` is supplied), the API uses it — scoped to your
            merchant — to find or create the matching offer session, so
            repeated `/analyze` calls on the same cart re-use the same
            session.

            This is **not** the same as `cart.id`. `cart.id` is the
            on-platform identifier you echo back inside the cart payload
            for analytics and debugging; it has no idempotency role.
            `cartId` (top-level) is the merchant-scoped session key.
        cart:
          $ref: '#/components/schemas/Cart'
        customer:
          $ref: '#/components/schemas/Customer'
        deliveryGroups:
          type: array
          description: |
            Delivery groups for the cart, if available from the checkout
            context. Used to detect pickup / local-delivery selections that
            disqualify the cart from coverage.
          items:
            $ref: '#/components/schemas/DeliveryGroup'
        offerOrigin:
          type: string
          description: |
            Where the protection offer is being presented (e.g. `"checkout"`,
            `"cart"`). Defaults to `"custom"` when omitted; surfaces in
            analytics and is not used for eligibility evaluation.
          example: checkout
        mountTarget:
          type: string
          description: |
            UI mount location for the offer (e.g. `"below_shipping"`,
            `"order_summary"`). Defaults to `"custom"` when omitted;
            surfaces in analytics and is not used for eligibility.
          example: below_shipping
        cartExperience:
          type: string
          description: |
            UI experience variant identifier (e.g. `"widget"`, `"modal"`).
            Defaults to `"api"` when omitted; surfaces in analytics only.
          example: widget
    # ---------------------------------------------------------------------------
    # Cart and line items
    # ---------------------------------------------------------------------------
    Cart:
      type: object
      required:
        - lines
        - itemCount
        - subtotalAmount
        - discountedSubtotalAmount
        - totalDiscountAmount
        - totalTaxAmount
        - totalAmount
        - currencyCode
      properties:
        id:
          type: string
          description: |
            On-platform cart identifier. Echoed verbatim and surfaced in
            analytics and debug output; it has **no idempotency role** —
            sending only `cart.id` will not re-use a previous offer
            session. For idempotent re-analysis, pass a stable identifier
            at the top level as `cartId` (or send a `sessionId`).
        token:
          type: string
          description: Cart token, if different from `id`.
        lines:
          type: array
          description: Line items in the cart.
          minItems: 1
          items:
            $ref: '#/components/schemas/CartLine'
        itemCount:
          type: integer
          description: Total number of items across all lines (sum of quantities).
          example: 3
        subtotalAmount:
          type: integer
          description: Cart subtotal before discounts, in minor currency units.
          example: 15000
        totalDiscountAmount:
          type: integer
          description: Total discount amount applied to the cart, in minor currency units.
        discountedSubtotalAmount:
          type: integer
          description: Subtotal after discounts, in minor currency units. Used as the basis for pricing.
          example: 13000
        totalTaxAmount:
          type: integer
          description: Total tax amount, in minor currency units.
        totalAmount:
          type: integer
          description: Total cart amount including tax and shipping, in minor currency units.
          example: 14040
        shippingAmount:
          type: integer
          description: Shipping cost, in minor currency units.
        currencyCode:
          type: string
          description: ISO 4217 currency code.
          example: USD
        baseCurrencyCode:
          type: string
          description: Base currency code for multi-currency stores.
        exchangeRate:
          type: number
          description: Exchange rate from base currency to cart currency.
        discounts:
          type: array
          description: Cart-level discounts.
          items:
            $ref: '#/components/schemas/Discount'
        taxLines:
          type: array
          description: Cart-level tax lines.
          items:
            $ref: '#/components/schemas/TaxLine'
        taxIncluded:
          type: boolean
          description: Whether tax is included in line item prices.
        attributes:
          type: object
          description: Arbitrary key-value attributes on the cart.
          additionalProperties:
            type: string
        createdAt:
          type: string
          format: date-time
          description: >
            ISO 8601 / RFC 3339 timestamp for when the cart was created
            (e.g. `"2026-04-25T13:25:00-05:00"`).
        updatedAt:
          type: string
          format: date-time
          description: >
            ISO 8601 / RFC 3339 timestamp for when the cart was last updated
            (e.g. `"2026-04-25T13:25:00-05:00"`).

    CartLine:
      type: object
      # Required matches analyze.validation.ts in loop-storefront-api.
      # variantId is optional; tags and attributes default to []/{} server-side.
      required:
        - id
        - productId
        - quantity
        - totalPrice
        - originalTotalPrice
        - title
      properties:
        id:
          type: string
          description: Line item identifier.
          example: line_001
        variantId:
          type: string
          description: Product variant identifier.
        productId:
          type: string
          description: Product identifier.
          example: prod_67890
        quantity:
          type: integer
          description: Quantity of this line item.
          minimum: 1
          example: 2
        unitPrice:
          $ref: '#/components/schemas/Money'
          description: Per-unit price. Optional — not all platforms expose per-unit pricing.
        totalPrice:
          $ref: '#/components/schemas/Money'
          description: The combined price of all items in the line item, including any line-level discounts.
        originalTotalPrice:
          $ref: '#/components/schemas/Money'
          description: The combined price of all items in the line item, excluding line-level discounts.
        title:
          type: string
          description: Product title.
          example: Classic T-Shirt
        variantTitle:
          type: string
          description: Variant title (e.g., size/color).
        handle:
          type: string
          description: Product handle or slug.
        vendor:
          type: string
          description: Product vendor.
        productType:
          type: string
          description: Product type or category.
        tags:
          type: array
          description: Product tags.
          items:
            type: string
        sku:
          type: string
          description: SKU for the variant.
        discounts:
          type: array
          description: Line-item discounts.
          items:
            $ref: '#/components/schemas/Discount'
        taxLines:
          type: array
          description: Line-item tax lines.
          items:
            $ref: '#/components/schemas/TaxLine'
        attributes:
          type: object
          description: Arbitrary key-value attributes on the line item.
          additionalProperties:
            type: string
        requiresShipping:
          type: boolean
          description: Whether this item requires shipping.
        taxable:
          type: boolean
          description: Whether this item is taxable.
        giftCard:
          type: boolean
          description: Whether this item is a gift card.

    Money:
      type: object
      required:
        - amount
        - currencyCode
      properties:
        amount:
          type: integer
          description: Amount in minor currency units (e.g., cents).
          example: 2500
        currencyCode:
          type: string
          description: ISO 4217 currency code.
          example: USD

    Discount:
      type: object
      required:
        - amount
      properties:
        id:
          type: string
          description: Discount identifier.
        title:
          type: string
          description: Discount title or description.
        amount:
          type: integer
          description: Discount amount in minor currency units.
          example: 2000
        type:
          type: string
          description: Type of discount.
          enum:
            - percentage
            - fixed_amount
            - shipping
            - other
        code:
          type: string
          description: Discount code, if applicable.

    TaxLine:
      type: object
      required:
        - amount
      properties:
        title:
          type: string
          description: Tax line title.
        amount:
          type: integer
          description: Tax amount in minor currency units.
          example: 1040
        rate:
          type: number
          description: Tax rate as a decimal (e.g., `0.08` for 8%).
        jurisdiction:
          type: string
          description: Tax jurisdiction.

    # ---------------------------------------------------------------------------
    # Customer
    # ---------------------------------------------------------------------------
    Customer:
      type: object
      properties:
        id:
          type: string
          description: Customer identifier on your platform.
        isB2B:
          type: boolean
          description: Whether this is a B2B customer. B2B customers may be excluded from eligibility.
        tags:
          type: array
          description: Customer tags.
          items:
            type: string
        locale:
          type: string
          description: Customer locale.
        orderCount:
          type: integer
          description: Total number of orders placed by this customer.
        totalSpent:
          $ref: '#/components/schemas/Money'
        acceptsMarketing:
          type: boolean
          description: Whether the customer has opted in to marketing.
        createdAt:
          type: string
          format: date-time
          description: >
            ISO 8601 / RFC 3339 timestamp for when the customer account was
            created (e.g. `"2026-04-25T13:25:00-05:00"`).
        attributes:
          type: object
          description: Arbitrary key-value attributes on the customer.
          additionalProperties:
            type: string

    # ---------------------------------------------------------------------------
    # Delivery groups
    # ---------------------------------------------------------------------------
    DeliveryGroup:
      type: object
      required:
        - deliveryMethod
      properties:
        id:
          type: string
          description: Delivery group identifier.
        deliveryMethod:
          type: string
          description: |
            How the items in this group are being delivered. Common values:
            `SHIPPING`, `PICKUP`, `LOCAL_DELIVERY`, `DIGITAL`.
          example: SHIPPING
        selectedDeliveryOption:
          type: object
          description: The delivery option selected by the customer.
          properties:
            handle:
              type: string
            title:
              type: string
        targetedCartLineIds:
          type: array
          description: Cart line IDs that belong to this delivery group.
          items:
            type: string
        destinationCountry:
          type: string
          description: Destination country code (ISO 3166-1 alpha-2).
        destinationRegion:
          type: string
          description: Destination region or state.
        isDeliveryRequired:
          type: boolean
          description: Whether physical delivery is required.
        groupType:
          type: string
          description: Type of purchase group (e.g., `oneTimePurchase`, `subscription`).
        estimatedCost:
          $ref: '#/components/schemas/Money'

    # ---------------------------------------------------------------------------
    # Response
    # ---------------------------------------------------------------------------
    AnalyzeResponse:
      type: object
      required:
        - sessionId
        - eligible
        - reason
        - chargeInstructions
        - metadata
      properties:
        sessionId:
          type: string
          description: |
            Unique identifier for this offer session. The same value is
            returned for re-analysis of the same cart (matched by `sessionId`
            or `cartId`). Persist it alongside your cart state so subsequent
            calls can re-use it.

            **Order API platforms:** when the customer accepts, your integration
            must report **both** pieces of state on the order when it pushes to
            the [Order API](/api-reference/latest/orders/create-order):

            - `fees[].accepted_offer_mode` — activates coverage (see `mode`
              below for the value to pass).
            - A `metadata` entry with `key: "session_id"` and `value` set to
              this `sessionId` — links the order to this offer session.

            **Shopify:** this `sessionId` is also returned ready-to-write in the
            top-level `cartAttributes` map (`_loop_session_id`); set it on the
            cart and Loop links the ingested order to the session — no Order API
            call is required.
          pattern: '^lop_[A-Za-z0-9]{20}$'
          example: lop_a1b2c3d4e5f6g7h8i9j0
        eligible:
          type: boolean
          description: Whether the cart is eligible for order protection.
          example: true
        reason:
          type: string
          description: |
            Why the cart is or isn't eligible. When `eligible` is `true`, this is
            `"eligible"`. When `false`, it describes the disqualifying condition.
          enum:
            - eligible
            - configuration_missing
            - configuration_invalid
            - order_protection_disabled
            - empty_cart
            - cart_only_has_coverage_products
            - cart_only_has_excluded_products
            - cart_has_excluded_product
            - b2b_customer_excluded
            - pickup_delivery_selected
            - cart_total_too_low
            - cart_too_valuable
            - locale_not_supported
            - currency_not_supported
            - ab_test_affects_attachment_rate_control_group
            - rule_evaluation_error
            - system_error
          example: eligible
        mode:
          type: array
          description: |
            The protection coverage(s) offered. A subset of
            `["returnCoverage", "shippingProtection"]`. Combined coverage is
            represented as the two-element array
            `["returnCoverage", "shippingProtection"]`.

            Present only when `eligible` is `true`. Omitted from the
            response when the cart is ineligible — do not assume this field
            is always set.

            When the customer accepts and your integration reports the fee
            via the [Order API](/api-reference/latest/orders/create-order),
            pass this array verbatim as `fees[].accepted_offer_mode`.
          items:
            type: string
            enum:
              - returnCoverage
              - shippingProtection
          example:
            - returnCoverage
        chargeInstructions:
          description: |
            Instructions for applying the protection charge to the cart. Present
            when the cart is eligible; `null` when ineligible.
          oneOf:
            - $ref: '#/components/schemas/ChargeInstructions'
            - type: 'null'
        cartAttributes:
          type: object
          additionalProperties:
            type: string
          description: |
            **Shopify only.** Cart attributes to set on the Shopify cart,
            returned for the `shopify` platform when the cart is eligible.
            Pre-formatted by Loop — copy verbatim onto the cart rather than
            constructing the keys yourself.

            Currently carries `_loop_session_id`, which links the resulting
            order to its offer session. Because it is cart-scoped, set it in
            **both** the accept and decline cases. Omitted for Order API
            platforms (`woocommerce`, `bigcommerce`, `custom`), which report
            the session id via the Order API `session_id` metadata instead.
          example:
            _loop_session_id: lop_a1b2c3d4e5f6g7h8i9j0
        metadata:
          type: object
          required:
            - priceBreakdown
            - experimentCohort
            - sessionCreated
            - analysisCount
          properties:
            priceBreakdown:
              type: array
              description: Breakdown of how the protection price was calculated.
              items:
                $ref: '#/components/schemas/PriceBreakdownItem'
            pricingSubtotalUsed:
              type: integer
              description: The subtotal amount used for pricing tier lookup, in minor currency units.
            conversionRate:
              type: number
              description: Currency conversion rate applied, if any.
            experimentCohort:
              description: |
                A/B test cohort assignment for this session. `null` when the
                merchant has no active experiment or `experimentationConsent`
                was not granted.
              oneOf:
                - $ref: '#/components/schemas/ExperimentCohort'
                - type: 'null'
            sessionCreated:
              type: boolean
              description: |
                `true` when this call created a new offer session, `false`
                when an existing session was re-analyzed (matched by
                `sessionId` or `cartId`).
              example: true
            analysisCount:
              type: integer
              minimum: 1
              description: |
                How many times this offer session has been analyzed,
                including the current call. `1` on first creation.
              example: 1
        debug:
          description: Verbose diagnostics. Only present when `debug` was `true` in the request.
          $ref: '#/components/schemas/DebugMetadata'

    SessionResponse:
      description: |
        The persisted state of an offer session, returned by
        `GET /v1/session/{sessionId}`. Mirrors `AnalyzeResponse` but
        includes `createdAt` / `updatedAt` and omits the
        request-driven `metadata.sessionCreated` flag.
      type: object
      required:
        - sessionId
        - eligible
        - reason
        - chargeInstructions
        - createdAt
        - updatedAt
        - metadata
      properties:
        sessionId:
          type: string
          description: Unique identifier for this offer session.
          pattern: '^lop_[A-Za-z0-9]{20}$'
          example: lop_a1b2c3d4e5f6g7h8i9j0
        eligible:
          type: boolean
          description: Whether the cart was eligible for order protection at the time of the most recent analysis.
        reason:
          type: string
          description: >
            The eligibility reason from the most recent analysis. See
            `AnalyzeResponse.reason` for the full enum.
          enum:
            - eligible
            - configuration_missing
            - configuration_invalid
            - order_protection_disabled
            - empty_cart
            - cart_only_has_coverage_products
            - cart_only_has_excluded_products
            - cart_has_excluded_product
            - b2b_customer_excluded
            - pickup_delivery_selected
            - cart_total_too_low
            - cart_too_valuable
            - locale_not_supported
            - currency_not_supported
            - ab_test_affects_attachment_rate_control_group
            - rule_evaluation_error
            - system_error
        mode:
          type: array
          description: |
            The protection coverage(s) offered by the most recent analysis.
            Present only when `eligible` is `true` — omitted when the session
            is ineligible. See `AnalyzeResponse.mode` for the full semantics.
          items:
            type: string
            enum:
              - returnCoverage
              - shippingProtection
        chargeInstructions:
          description: Instructions for applying the protection charge, or `null` when ineligible.
          oneOf:
            - $ref: '#/components/schemas/ChargeInstructions'
            - type: 'null'
        cartAttributes:
          type: object
          additionalProperties:
            type: string
          description: >
            **Shopify only.** Cart attributes to set on the Shopify cart (e.g.
            `_loop_session_id`). See `AnalyzeResponse.cartAttributes` for the
            full semantics. Omitted for Order API platforms.
          example:
            _loop_session_id: lop_a1b2c3d4e5f6g7h8i9j0
        createdAt:
          type: string
          format: date-time
          description: >
            RFC 3339 / ATOM timestamp without milliseconds for when the
            session was first created.
          example: '2026-04-23T13:25:00+00:00'
        updatedAt:
          type: string
          format: date-time
          description: >
            RFC 3339 / ATOM timestamp without milliseconds for when the
            session was last analyzed.
          example: '2026-04-23T13:27:14+00:00'
        metadata:
          type: object
          required:
            - priceBreakdown
            - experimentCohort
            - analysisCount
          properties:
            priceBreakdown:
              type: array
              items:
                $ref: '#/components/schemas/PriceBreakdownItem'
            pricingSubtotalUsed:
              type: integer
              description: Subtotal amount used for pricing tier lookup, in minor currency units.
            conversionRate:
              type: number
              description: Currency conversion rate applied, if any.
            experimentCohort:
              description: A/B test cohort assignment for this session, or `null` when none.
              oneOf:
                - $ref: '#/components/schemas/ExperimentCohort'
                - type: 'null'
            analysisCount:
              type: integer
              minimum: 1
              description: Total number of analyses performed against this session.

    # ---------------------------------------------------------------------------
    # Charge instructions
    # ---------------------------------------------------------------------------
    ChargeInstructions:
      description: |
        Discriminated union on `method`. Loop returns the variant that
        matches the merchant's configured integration:

        - `fee` — apply the charge as an order-level fee using your
          platform's native fee mechanism (Order API platforms).
        - `product_line_item` — apply the charge by adding a dedicated
          coverage product to the cart as a line item (Shopify).

        For Order API platforms, acceptance is reported by your integration via
        `fees[].accepted_offer_mode` on the
        [Order API](/api-reference/latest/orders/create-order). For Shopify,
        acceptance is signalled on the cart (the coverage line item with its
        `lineItemProperties`, plus the top-level `cartAttributes`) and Loop
        ingests the order directly.
      oneOf:
        - $ref: '#/components/schemas/FeeChargeInstructions'
        - $ref: '#/components/schemas/ProductLineItemChargeInstructions'
      discriminator:
        propertyName: method
        mapping:
          fee: '#/components/schemas/FeeChargeInstructions'
          product_line_item: '#/components/schemas/ProductLineItemChargeInstructions'

    FeeChargeInstructions:
      title: Non-Shopify platforms
      type: object
      required:
        - amount
        - currencyCode
        - method
      properties:
        amount:
          type: integer
          minimum: 0
          description: Charge amount in minor currency units.
          example: 298
        currencyCode:
          type: string
          description: ISO 4217 currency code for the charge.
          example: USD
        method:
          type: string
          enum:
            - fee
          description: Charge is applied as an order-level fee.
          example: fee

    ProductLineItemChargeInstructions:
      title: Shopify
      type: object
      required:
        - amount
        - currencyCode
        - method
        - product
        - lineItemProperties
      properties:
        amount:
          type: integer
          minimum: 0
          description: Charge amount in minor currency units.
          example: 298
        currencyCode:
          type: string
          description: ISO 4217 currency code for the charge.
          example: USD
        method:
          type: string
          enum:
            - product_line_item
          description: Charge is applied by adding a coverage product to the cart as a hidden line item (Shopify only).
          example: product_line_item
        product:
          type: object
          required:
            - id
            - handle
          properties:
            id:
              type: string
              description: Platform product identifier for the coverage product.
              example: '8001234567890'
            handle:
              type: string
              description: Product handle / slug for the coverage product.
              example: loop-order-protection
        lineItemProperties:
          type: object
          additionalProperties:
            type: string
          description: |
            Line item properties to set on the coverage product line when the
            customer accepts. Pre-formatted by Loop — copy verbatim onto the
            line item rather than deriving the keys or values yourself.

            Currently carries `_mode` (the accepted coverage type, e.g.
            `"returnCoverage,shippingProtection"`). Loop owns the key names,
            value formatting, and Shopify privacy conventions; treat this as an
            opaque block of properties to apply.
          example:
            _mode: returnCoverage,shippingProtection

    # ---------------------------------------------------------------------------
    # Metadata types
    # ---------------------------------------------------------------------------
    PriceBreakdownItem:
      type: object
      required:
        - type
        - amount
      properties:
        type:
          type: string
          description: Component of the price breakdown.
          example: base
        amount:
          type: integer
          description: Amount in minor currency units.
          example: 298
        label:
          type: string
          description: Human-readable label for this breakdown component.

    ExperimentCohort:
      type: object
      required:
        - experimentId
        - group
        - bucket
        - assignedAt
      properties:
        experimentId:
          type: string
          description: Identifier for the experiment.
          example: cp_attachment_q2
        group:
          type: string
          description: The cohort group assigned (e.g., `control`, `treatment`).
          example: treatment
        bucket:
          type: integer
          nullable: true
          description: Numeric bucket assignment, or `null` when not bucketed.
        assignedAt:
          type: integer
          description: |
            Unix timestamp in **milliseconds** for when the cohort was
            assigned. Note that this is an integer epoch value, not the
            RFC 3339 / ATOM string format used by the other timestamp
            fields on this API.
          example: 1761225900000
        experimentSessionId:
          type: string
          description: Internal identifier correlating this assignment with the experiment platform.
        metadata:
          type: object
          description: Additional experiment metadata.
          additionalProperties: true

    DebugMetadata:
      type: object
      properties:
        eligibility:
          type: object
          description: Eligibility evaluation diagnostics.
          properties:
            rulesEvaluated:
              type: array
              description: Names of eligibility rules that were evaluated.
              items:
                type: string
            failedRule:
              type: string
              description: The rule that caused ineligibility, if any.
            failedRuleDescription:
              type: string
              description: Human-readable description of the failed rule.
            failedRuleContext:
              type: object
              description: Additional context about why the rule failed.
              additionalProperties: true
            evaluatedAt:
              type: string
              format: date-time
              description: >
                RFC 3339 / ATOM timestamp without milliseconds for when
                eligibility was evaluated.
              example: '2026-04-25T13:25:00+00:00'
            processingTimeMs:
              type: number
              description: Time spent evaluating eligibility rules (ms).
        pricing:
          type: object
          description: Pricing calculation diagnostics.
          properties:
            processingTimeMs:
              type: number
              description: Time spent calculating pricing (ms).
            cartSnapshot:
              type: object
              description: Summary of the cart data used for pricing.
              properties:
                subtotalAmount:
                  type: integer
                discountedSubtotalAmount:
                  type: integer
                totalAmount:
                  type: integer
                lineItemCount:
                  type: integer
                currencyCode:
                  type: string
        configuration:
          type: object
          description: Merchant configuration used for this analysis.
          properties:
            protectionMode:
              type: string
              description: Raw protection mode value from merchant configuration.
            resolvedModes:
              type: array
              description: |
                The protection modes resolved from the merchant configuration
                — the same shape as the top-level `mode[]` field on the
                response.
              items:
                type: string
                enum:
                  - returnCoverage
                  - shippingProtection
            supportedLocales:
              type: array
              items:
                type: string
            supportedCurrencies:
              type: array
              items:
                type: string
            excludedProductTags:
              type: array
              items:
                type: string
            maxCoverageAmount:
              type: integer
        totalProcessingTimeMs:
          type: number
          description: Total server-side processing time (ms).

    # ---------------------------------------------------------------------------
    # Errors
    # ---------------------------------------------------------------------------
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: validation_error
            message:
              type: string
              description: Human-readable error description.
              example: Request validation failed
            details:
              type: object
              description: Per-field validation errors, when applicable.
              additionalProperties:
                type: string

  headers:
    X-Request-Id:
      description: |
        Unique identifier for this API call. Returned on every Checkout+ API
        response — success or error — so you can correlate client behavior
        with Loop's server-side logs. Include this value when contacting
        support about a specific request.
      schema:
        type: string
        example: req_abc123def456

  securitySchemes:
    api_key:
      type: apiKey
      in: header
      name: X-Authorization
      description: |
        Merchant API key. See [Authentication](https://docs.loopreturns.com/api-reference/authentication)
        for setup, scopes, and error handling.
