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

> ## Agent Instructions
> For new direct REST integrations, use the v2026-04 surfaces. The @fluid-app FairShare SDK continues to use its own published public-v2025-06 contract.
> Authenticate with the header Authorization: Bearer <token>; public storefront read endpoints require no auth.
> Lists use cursor pagination via the page[cursor] and page[limit] query params; follow meta.pagination.next_cursor until it is null.
> Never use /api/company/v1 or /api/v1 paths, page/per_page params, or offset pagination — they are legacy.
> The OpenAPI specs under api-reference/ are the authoritative contracts; prefer them over prose when in doubt. api-reference/storefront-v2026-04.yaml covers the v2026-04 storefront surface (/api/v202604/... paths); api-reference/auth-v0.yaml covers the unversioned auth surface (/api/... paths — authentication, MFA, social auth, and token exchange); api-reference/checkout-v2026-04.yaml covers the v2026-04 checkout surface (/api/checkout/v2026-04/... paths — carts, cart auth, discounts, items, subscriptions, orders, enrollments, and store config); api-reference/public-v2025-06.yaml covers the Public SDK surface used by the @fluid-app FairShare SDK, including its parallel cart lifecycle, browser integrations, versioned payment callbacks, unversioned public utilities, and the cart price-override operation; api-reference/payment-v2026-04.yaml covers the v2026-04 payment gateway admin surface (/api/payment/v2026-04/... paths, bearer-authenticated — gateway CRUD, gateway purchase/authorize/$0-verify, transaction list/show and capture/void/credit, and merchant payment configuration); api-reference/payments-v2026-04.yaml covers the v2026-04 cart payment surface (/api/payments/v2026-04/carts/{cart_token}/... paths, authenticated by the cart token in the path with no bearer — payment-method selection, VGS card tokenization, 3D Secure verification, and PayPal/Braintree/Klarna/Apple Pay flows); api-reference/commerce-v2026-04.yaml covers the v2026-04 commerce order-editing surface (/api/v202604/orders/{order_id}/edits paths, bearer-authenticated — post-checkout order edits that atomically insert items and add adjustments/discounts, with an optional dry-run preview); api-reference/webhooks-v0.yaml covers the unversioned webhooks surface (/api/... paths — webhook registration, delivery payloads, callback registrations, company events, and webhook/callback schemas).
> Successful responses wrap the resource payload alongside a top-level integer status and a meta object.

# Company Products index

> Token-authenticated. Returns EVERY Product for the calling company
regardless of lifecycle (draft / archived rows included), so admins
can manage non-public rows.

Supports the same cursor pagination / filter / sort / `q` contract
as the public catalog. `filter[status]` (the raw Product enum keys
`active`/`draft`/`archived`) is only meaningful on this surface.
Product price is COUNTRY-RELATIVE — the `country` parameter selects
the per-country variant price/currency rendered.




## OpenAPI

````yaml /api-reference/storefront-v2026-04.yaml get /api/v202604/company/products
openapi: 3.1.0
info:
  title: Fluid Storefront v2026-04 API
  version: v2026-04
  description: |
    The modernized storefront surface, covering all eight storefront
    resources: **Categories**, **Collections**, **Products**, **Posts**,
    **Pages**, **Media**, **Enrollment Packs**, and **Playlists**.

    ## Two surfaces

    Every resource is exposed twice, and the pair is the thing to
    understand first:

    - **Public** — `/api/v202604/{resource}` and
      `/api/v202604/{resource}/{slug}`. Unauthenticated
      (`security: []`); the store is resolved from the request
      subdomain. Addressed by `slug`, the same identifier that appears
      in `canonical_url`. Returns only live rows — non-live content
      never leaks here.
    - **Company** — `/api/v202604/company/{resource}`. Bearer-token
      authenticated, addressed by `id`, and returns every row whatever
      its lifecycle state, so an admin can manage drafts and archives.
      Full CRUD plus the standardized `shares`, `lighthouse`, and
      `compliance` member actions on each resource.

    Path segments use the storefront's nouns, not the database's:
    `enrollment-packs` and `playlists` (the Library model).

    ## One response shape

    Every resource response opens with the same canonical "common top",
    in the same order, before any resource-specific fields:

        id, slug, title, description, image_url, canonical_url, images,
        active, status, publish_at, seo, metafields, languages

    That means a consumer can render any storefront resource from one
    code path. `seo` is always present and always fully populated — the
    server resolves it through a priority chain (SEO record → resource
    field → store default → built-in), so there is no client-side
    fallback logic and the API can never disagree with the rendered
    `<head>`. `metafields`, `languages`, and (where applicable)
    `countries` are always arrays, never null.

    ## Lifecycle

    `status` is the canonical four-value contract — `draft`,
    `scheduled`, `published`, `archived` — and resolves at the API
    boundary: a stored `scheduled` row whose `publish_at` has passed
    reads as `published` with no write required. `active` is the
    separate merchant on/off toggle; a resource is live only when it is
    both active and resolved-published. Company create and update
    auto-queue Lighthouse and compliance scans, so the scores on the
    member actions always describe the current state of the resource.

    ## Listing and pagination

    Index endpoints share one query surface: `lang`, `q` (Elasticsearch
    over title and description), `filter[…]`, `sort`, and cursor
    pagination via `page[cursor]` / `page[limit]` (default 25, max 100).
    Follow `meta.pagination.next_cursor`; do not construct cursors.
    Products additionally take `country`, which selects the
    country-relative price and currency AND gates availability.

    Categories and Collections expose their products from their own
    path — `GET /api/v202604/categories/{slug}/products` — which is the
    products catalog scoped to one owner, with the same card shape,
    filters, sorting, and pagination.

    ## Translating a resource

    Translations are written inline on the resource's own create/update.
    There is no separate translations endpoint and no `translations`
    object in the payload — the `lang` query parameter selects the locale
    a request reads or writes.

    **Read** a locale with `lang` on any GET:

        GET /api/v202604/company/collections/4821?lang=fr

    Translated fields (`title`, `description`, `image_url`, and each
    resource's own translated fields) come back inline in that locale.
    On the public surface any field with no `fr` translation falls back
    to its `en` value, so a partially-translated resource never renders
    blank. The `languages` array on every response lists exactly the
    locales that carry a translation, always including the default.

    **Write** a locale with `lang` on POST/PATCH:

        PATCH /api/v202604/company/collections/4821?lang=fr
        { "collection": { "title": "Essentiels bien-être" } }

    Omitting `lang` writes the store's default locale — the store's own
    default language, not the platform's. A store selling in French
    treats `lang=fr` as its default write.

    A translation PATCH is TEXT ONLY: against any other locale,
    everything but the translated fields is ignored, so translating a
    title can never regenerate the slug, move `status`, or change
    `country_isos`, metafields, SEO, or associations. Send those in a
    request against the store's default locale instead.

    Creating is different: POST always writes the store's default locale
    whatever `lang` says, and keeps its full payload. A resource created
    under a translation would have no value in the store's own language
    and would take its slug from the translation. `lang` is still
    validated on POST, so an unsupported locale is a 422 rather than a
    surprise.

    A `lang` the store does not sell in returns 422 — translations are
    never written to a locale the store has not enabled. Add the language
    to the store first.

    The typical flow is: create in the default locale, then one PATCH per
    additional locale carrying only the translated text.
  contact:
    email: support@fluid.app
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://api.fluid.app
  - url: https://{company}.fluid.app
    description: Production server with company subdomain
    variables:
      company:
        default: myco
        description: Company subdomain
security: []
tags:
  - name: storefront
    description: |
      Unauthenticated public surface that backs storefront pages and
      AEO crawlers. All operations are `security: []`.
  - name: company
    description: |
      Token-authenticated company surface that backs the admin UI and
      integrations.
paths:
  /api/v202604/company/products:
    get:
      tags:
        - company
      summary: Company Products index
      description: |
        Token-authenticated. Returns EVERY Product for the calling company
        regardless of lifecycle (draft / archived rows included), so admins
        can manage non-public rows.

        Supports the same cursor pagination / filter / sort / `q` contract
        as the public catalog. `filter[status]` (the raw Product enum keys
        `active`/`draft`/`archived`) is only meaningful on this surface.
        Product price is COUNTRY-RELATIVE — the `country` parameter selects
        the per-country variant price/currency rendered.
      operationId: companyProductsIndex
      parameters:
        - $ref: '#/components/parameters/Lang'
        - $ref: '#/components/parameters/Country'
        - $ref: '#/components/parameters/Q'
        - $ref: '#/components/parameters/PageCursor'
        - $ref: '#/components/parameters/PageLimit'
        - name: filter[status]
          in: query
          required: false
          description: Raw Product enum key.
          schema:
            type: string
            enum:
              - active
              - draft
              - archived
        - name: filter[availability]
          in: query
          required: false
          description: |
            Stock filter. `in_stock` restricts to purchasable products; `all`
            (default) imposes no stock constraint.
          schema:
            type: string
            enum:
              - all
              - in_stock
            default: all
        - name: filter[bundle]
          in: query
          required: false
          description: |
            `true` returns only bundle products; `false` returns only non-bundle
            products.
          schema:
            type: boolean
        - name: filter[category_ids]
          in: query
          required: false
          description: |
            Restrict to products in any of these category ids.
          schema:
            type: array
            items:
              type: integer
        - name: filter[collection_ids]
          in: query
          required: false
          description: |
            Restrict to products in any of these collection ids.
          schema:
            type: array
            items:
              type: integer
        - name: sort
          in: query
          required: false
          description: >
            Single sort key. Prefix with `-` for descending. Default `priority`
            then

            `id`.
          schema:
            type: string
            enum:
              - priority
              - '-priority'
              - title
              - '-title'
              - created_at
              - '-created_at'
      responses:
        '200':
          description: A page of products.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
      security:
        - bearer_auth: []
components:
  parameters:
    Lang:
      name: lang
      in: query
      required: false
      description: |
        Response locale. ISO code (e.g. `fr`, `de`). When omitted,
        responds in the company's default locale. On the public
        surface, falls back to `en` for any translated field missing
        in the requested locale.
      schema:
        type: string
        example: fr
    Country:
      name: country
      in: query
      required: false
      description: |
        ISO-2 country code that selects COUNTRY-RELATIVE Product pricing.
        Product-only — Categories and other storefront resources use
        `filter[country]` as an access-list visibility filter instead.

        For Products, `country` drives the `pricing` block and each
        variant's `variant_countries`: it picks which per-country variant
        price and currency are rendered. On the catalog it also drops
        products with no active variant in the requested country. When
        omitted, falls back to the company's default country.
      schema:
        type: string
        example: GB
    Q:
      name: q
      in: query
      required: false
      description: |
        Free-text search. Backed by Searchkick / Elasticsearch over
        translation-aware `title` and `description` fields.
      schema:
        type: string
    PageCursor:
      name: page[cursor]
      in: query
      required: false
      description: |
        Rotulus cursor returned in `meta.pagination.next_cursor` (or
        `prev_cursor`). Omit on the first page.
      schema:
        type: string
    PageLimit:
      name: page[limit]
      in: query
      required: false
      description: |
        Page size. Default 25. Max 100. Requests above 100 return 422.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
  schemas:
    ProductListResponse:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          required:
            - products
          properties:
            products:
              type: array
              items:
                $ref: '#/components/schemas/ProductCard'
            meta:
              allOf:
                - $ref: '#/components/schemas/Meta'
                - required:
                    - pagination
    Envelope:
      type: object
      description: |
        Shared success envelope for list / show / create / update
        responses: the resource payload is returned alongside a top-level
        integer `status` and `meta`. Composed onto each resource response
        via `allOf`.
      required:
        - status
        - meta
      properties:
        status:
          type: integer
          example: 200
        meta:
          $ref: '#/components/schemas/Meta'
    ProductCard:
      type: object
      description: |
        The LEAN catalog card returned by the Products index (and the AEO
        `.json` catalog). It is the default ProductBlueprint view: the
        canonical common top MINUS the heavy SEO-ish fields the card
        deliberately omits (`description`, `image_url`, `seo`) plus the
        commerce SUMMARY (`sku`, `is_bundle`, `in_stock`,
        `has_subscription_plans`, `pricing`, `default_variant`). The full
        `description` / `image_url` / `seo` and the heavy commerce tail
        (`variants`, `subscription_plans`, `media`, `bundle_groups`) are
        reserved for the single-record show payload (see `Product`), keeping
        the catalog N+1-free regardless of page size.
      required:
        - id
        - slug
        - title
        - canonical_url
        - images
        - active
        - status
        - publish_at
        - metafields
        - languages
        - sku
        - is_bundle
        - in_stock
        - has_subscription_plans
        - pricing
        - default_variant
      properties:
        id:
          type: integer
          example: 60311
        slug:
          type:
            - string
            - 'null'
          description: >-
            Null for legacy products created without a slug (the column is
            nullable).
          example: scrute-farms-beet-blend
        title:
          type: string
          example: Schrute Farms Beet Blend
        canonical_url:
          type: string
          example: https://acme.fluid.app/home/products/scrute-farms-beet-blend
        images:
          $ref: '#/components/schemas/ResponsiveImageSet'
        active:
          type: boolean
          description: |
            Derived merchant on/off toggle: `true` when the product is not
            archived. When `false`, the product is hidden from EVERY public
            surface regardless of `status`.
          example: true
        status:
          type: string
          enum:
            - draft
            - scheduled
            - published
            - archived
          description: |
            Lifecycle, mapped at read time from the Product's integer
            `status` enum plus `publish_at`. "Live" = `active == true` AND
            resolved `status == published`.
          example: published
        publish_at:
          type:
            - string
            - 'null'
          format: date-time
          example: null
        metafields:
          type: array
          items:
            $ref: '#/components/schemas/Metafield'
        languages:
          type: array
          items:
            type: string
          description: |
            ISO codes of locales this product is translated into. Always
            includes the company default locale; never empty.
          example:
            - en
            - fr
        sku:
          type:
            - string
            - 'null'
          description: |
            The product-level stock-keeping unit. Null when the merchant
            tracks SKUs per variant only.
          example: SCRUTE-BEET-001
        is_bundle:
          type: boolean
          description: |
            True when this product is a bundle assembled from bundle groups;
            its price is the configured bundle price, not a variant price.
          example: false
        in_stock:
          type: boolean
          description: |
            True when at least one variant is purchasable for some country.
            Mirrors the merchandising stock flag.
          example: true
        has_subscription_plans:
          type: boolean
          description: |
            True when the product has at least one active subscription plan,
            so the storefront can surface subscribe-and-save options.
          example: true
        pricing:
          type:
            - object
            - 'null'
          description: |
            COUNTRY-RELATIVE retail pricing for the requested `country`
            (defaults to the company's default country). Always the
            consumer-facing RETAIL price — never wholesale. `null` when the
            product has no resolvable price in any country.
          required:
            - price
            - currency_code
            - compare_at
            - subscription_price
            - display_price
          properties:
            price:
              type:
                - string
                - 'null'
              description: >-
                Numeric retail unit price in the country's currency, serialized
                as a decimal string.
              example: '29.99'
            currency_code:
              type:
                - string
                - 'null'
              description: ISO 4217 currency code for the resolved country.
              example: USD
            compare_at:
              type:
                - string
                - 'null'
              description: |
                Strike-through "compare at" price when the merchant set one,
                serialized as a decimal string; null otherwise. Always null
                for bundle products.
              example: '39.99'
            subscription_price:
              type:
                - string
                - 'null'
              description: |
                Per-cycle subscription price when the product has a
                subscription plan, serialized as a decimal string; null
                otherwise. Always null for bundle products.
              example: '26.99'
            display_price:
              type:
                - string
                - 'null'
              description: |
                Pre-formatted, currency-symbol price string ready to render
                in the storefront.
              example: $29.99
        default_variant:
          type:
            - object
            - 'null'
          description: |
            Lightweight master-variant summary (id + sku) for the catalog
            card. The full variant payload is on the show (`Product`) view.
            Null when the product has no master variant.
          required:
            - id
            - sku
          properties:
            id:
              type: integer
              example: 880912
            sku:
              type:
                - string
                - 'null'
              example: SCRUTE-BEET-001-12OZ
    Meta:
      type: object
      description: |
        Response metadata included on every successful response body.
        `request_id` and `timestamp` are always present; list endpoints
        also add `pagination`.
      required:
        - request_id
        - timestamp
      properties:
        request_id:
          type:
            - string
            - 'null'
        timestamp:
          type: string
          format: date-time
        pagination:
          $ref: '#/components/schemas/Pagination'
    UnauthorizedError:
      type: object
      description: |
        Bare 401 body — a single `message` string, not the wrapped
        `ErrorResponse`.
      required:
        - message
      properties:
        message:
          type: string
          example: Invalid credentials.
    ForbiddenError:
      description: |
        Bare 403 body: `{ error }` when a storefront permission is missing,
        or `{ message }` when the credential is not company-admin tier.
      oneOf:
        - type: object
          required:
            - error
          properties:
            error:
              type: string
              example: Not authorized.
        - type: object
          required:
            - message
          properties:
            message:
              type: string
              example: Not authorized.
    ErrorResponse:
      type: object
      required:
        - error
        - status
        - meta
      properties:
        error:
          type: object
          required:
            - message
          properties:
            message:
              type: string
            details:
              type: object
              additionalProperties: true
        status:
          type: integer
        meta:
          $ref: '#/components/schemas/Meta'
    ResponsiveImageSet:
      type:
        - object
        - 'null'
      description: |
        Three ImageKit-transformed renditions of the resource's
        primary image: 200/600/1200 wide, scale-down only, format
        auto-negotiated. `null` when the resource has no image.
      properties:
        thumb:
          $ref: '#/components/schemas/ImageRendition'
        medium:
          $ref: '#/components/schemas/ImageRendition'
        large:
          $ref: '#/components/schemas/ImageRendition'
    Metafield:
      type: object
      required:
        - namespace
        - key
        - value
        - value_type
      properties:
        namespace:
          type: string
          example: custom
        key:
          type: string
          example: material
        value:
          example: cotton
        value_type:
          type: string
          example: single_line_text_field
        description:
          type:
            - string
            - 'null'
          example: The primary material used in this product
        created_at:
          type: string
          format: date-time
          example: '2026-05-01T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-05-01T12:00:00Z'
        locked:
          type:
            - boolean
            - 'null'
          example: false
    Pagination:
      type: object
      required:
        - cursor
        - limit
        - prev_cursor
        - next_cursor
      properties:
        cursor:
          type:
            - string
            - 'null'
        limit:
          type: integer
          example: 25
        prev_cursor:
          type:
            - string
            - 'null'
        next_cursor:
          type:
            - string
            - 'null'
    ImageRendition:
      type: object
      required:
        - url
        - width
      properties:
        url:
          type: string
          example: >-
            https://ik.imagekit.io/fluidapp/tr:w-600,c-at_max,f-auto,q-75/categories/4821.png
        width:
          type: integer
          example: 600
  responses:
    Unauthorized:
      description: |
        Missing or invalid bearer token. The body is a bare `{ message }`
        object — not the wrapped `ErrorResponse` envelope used by 404 / 422.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/UnauthorizedError'
    Forbidden:
      description: |
        Authenticated but lacking the required permission. The body is
        bare — `{ error }` when the storefront permission is missing, or
        `{ message }` when the credential is not company-admin tier — not
        the wrapped `ErrorResponse` envelope used by 404 / 422.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ForbiddenError'
    UnprocessableEntity:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    bearer_auth:
      type: http
      scheme: bearer
      description: |
        Bearer token authentication. Accepts company-admin tokens with
        `storefront.view` for read operations and `storefront.update`
        for writes and scan triggers (legacy per-resource grants like
        `categories.update` / `products.view` / etc. are still accepted
        during the migration), required on every `company` operation.
        Storefront public operations under `/api/v202604/{resource}` and
        `/api/v202604/{resource}/{slug}` are unauthenticated
        (`security: []`).

````