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

# Public Categories catalog

> Unauthenticated. Company is resolved from the request subdomain.
Returns ONLY live categories (active + resolved-published,
country-eligible, language-eligible) — non-public rows never
leak.

Filters, search, and pagination are documented on each parameter.




## OpenAPI

````yaml /api-reference/storefront-v2026-04.yaml get /api/v202604/categories
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/categories:
    get:
      tags:
        - storefront
      summary: Public Categories catalog
      description: |
        Unauthenticated. Company is resolved from the request subdomain.
        Returns ONLY live categories (active + resolved-published,
        country-eligible, language-eligible) — non-public rows never
        leak.

        Filters, search, and pagination are documented on each parameter.
      operationId: storefrontCategoriesIndex
      parameters:
        - $ref: '#/components/parameters/Lang'
        - $ref: '#/components/parameters/Q'
        - $ref: '#/components/parameters/PageCursor'
        - $ref: '#/components/parameters/PageLimit'
        - $ref: '#/components/parameters/FilterCountry'
        - $ref: '#/components/parameters/FilterLanguage'
        - name: filter[source_type]
          in: query
          required: false
          description: >
            One source type: `product`, `medium`, `page`, `library`, or
            `promotion`.

            Categories only — the collection catalog does not accept this
            filter.
          schema:
            type: string
            enum:
              - product
              - medium
              - page
              - library
              - promotion
        - name: filter[parent_id]
          in: query
          required: false
          description: |
            A category id as a string, or the literal `"root"` for top-level
            categories only. Selects a single level of the tree. Categories
            only — collections have no hierarchy. Honored on both the public
            catalog and the company index, which share one query contract.
          schema:
            type: string
            example: root
        - $ref: '#/components/parameters/Sort'
      responses:
        '200':
          description: A page of live categories.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryListResponse'
              example:
                status: 200
                categories:
                  - id: 4821
                    slug: summer-sale
                    title: Summer Sale
                    description: Hand-picked categories for the July clearance.
                    image_url: https://cdn.fluid.app/categories/4821.png
                    canonical_url: https://acme.fluid.app/home/categories/summer-sale
                    images:
                      thumb:
                        url: >-
                          https://ik.imagekit.io/fluidapp/tr:w-200,c-at_max,f-auto,q-70/categories/4821.png
                        width: 200
                      medium:
                        url: >-
                          https://ik.imagekit.io/fluidapp/tr:w-600,c-at_max,f-auto,q-75/categories/4821.png
                        width: 600
                      large:
                        url: >-
                          https://ik.imagekit.io/fluidapp/tr:w-1200,c-at_max,f-auto,q-80/categories/4821.png
                        width: 1200
                    active: true
                    status: published
                    publish_at: null
                    seo:
                      id: 9215
                      title: Summer Sale — Up to 40% Off
                      description: Shop the July clearance event before it ends.
                      image_url: >-
                        https://ik.imagekit.io/fluidapp/tr:w-1200,h-630,c-maintain_ratio,fo-auto,f-auto,q-80/seo/9215.png
                      block_crawler: false
                    metafields: []
                    countries:
                      - GB
                      - US
                    languages:
                      - en
                      - fr
                    parent_id: null
                    position: 1
                    source_type: product
                    has_children: false
                  - id: 4822
                    slug: clearance
                    title: Clearance
                    description: Last-chance prices on overstock items.
                    image_url: null
                    canonical_url: https://acme.fluid.app/home/categories/clearance
                    images: null
                    active: true
                    status: published
                    publish_at: null
                    seo:
                      title: Clearance
                      description: Last-chance prices on overstock items.
                      image_url: >-
                        https://ik.imagekit.io/fluid/980191006/images/VJBWJ3/VJBWJ3_QDwktuK8Ji.webp
                      block_crawler: false
                    metafields: []
                    countries: []
                    languages:
                      - en
                    parent_id: null
                    position: 2
                    source_type: product
                    has_children: false
                meta:
                  request_id: 7f3c2a90-1c2b-4d5e-9aaa-112233445566
                  timestamp: '2026-07-15T12:34:56Z'
                  pagination:
                    cursor: null
                    next_cursor: eyJpZCI6NDgyMn0
                    prev_cursor: null
                    limit: 25
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
      security: []
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
    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
    FilterCountry:
      name: filter[country]
      in: query
      required: false
      description: |
        ISO-2 country code. Restricts the catalog to resources available
        in that country — combining the `with_country_iso` scope's
        OR-empty semantics: unrestricted resources (no `access_countries`
        rows) match every country.
      schema:
        type: string
        example: GB
    FilterLanguage:
      name: filter[language]
      in: query
      required: false
      description: |
        ISO locale. Restricts the catalog to resources translated in
        that locale.
      schema:
        type: string
        example: fr
    Sort:
      name: sort
      in: query
      required: false
      description: |
        Single sort key. Prefix with `-` for descending. Default
        `position` then `id`.
      schema:
        type: string
        enum:
          - position
          - '-position'
          - title
          - '-title'
          - created_at
          - '-created_at'
  schemas:
    CategoryListResponse:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          required:
            - categories
          properties:
            categories:
              type: array
              items:
                $ref: '#/components/schemas/Category'
            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'
    Category:
      description: |
        Public Category read shape — the storefront (unauthenticated)
        projection. Closed with `unevaluatedProperties: false` so any
        authenticated-only field (notably `custom_slug`) leaking onto the
        public surface is caught in conformance.
      allOf:
        - $ref: '#/components/schemas/CategoryBase'
      unevaluatedProperties: false
    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'
    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'
    CategoryBase:
      type: object
      description: |
        Storefront Category. The first thirteen fields are the canonical
        common top shared by every storefront resource in v202604 — same
        names, same order, same types across Categories, Collections,
        Products, Pages, Enrollment Packs, Posts, Media, Playlists.
      required:
        - id
        - slug
        - title
        - description
        - image_url
        - canonical_url
        - images
        - active
        - status
        - publish_at
        - seo
        - metafields
        - languages
        - parent_id
        - position
        - source_type
        - has_children
        - countries
      properties:
        id:
          type: integer
          description: >
            Numeric identifier for the category. This is the canonical
            company-surface

            identifier used to address the row on

            `/api/v202604/company/categories/{id}` endpoints.
          example: 4821
        slug:
          type: string
          description: >
            URL-safe identifier and the canonical public identifier for the
            category.

            It matches the row's `canonical_url` and is the key used by the
            public

            show endpoint (`GET /api/v202604/categories/{slug}`).
          example: summer-sale
        title:
          type: string
          description: |
            Human-readable display title of the category. When the slug is
            auto-generated, it derives from this value.
          example: Summer Sale
        description:
          type:
            - string
            - 'null'
          description: |
            Optional long-form description of the category; `null` when unset.
          example: Hand-picked categories for the July clearance.
        image_url:
          type:
            - string
            - 'null'
          description: >
            URL of the category's primary image, or `null` when it has no image.
            See

            `images` for responsive renditions.
          example: https://cdn.fluid.app/categories/4821.png
        canonical_url:
          type: string
          description: >
            Absolute canonical storefront URL for the category, built from its
            current

            `slug`.
          example: https://acme.fluid.app/home/categories/summer-sale
        images:
          $ref: '#/components/schemas/ResponsiveImageSet'
        active:
          type: boolean
          description: |
            Merchant on/off toggle. When false, the resource is hidden
            from EVERY public surface regardless of `status`.
          example: true
        status:
          type: string
          enum:
            - draft
            - scheduled
            - published
            - archived
          description: |
            Lifecycle. The auto-go-live rule applies at read time: a
            stored `scheduled` row whose `publish_at` has passed
            resolves to `published`. "Live" = `active == true` AND
            resolved `status == published`.
          example: published
        publish_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >
            Scheduled go-live timestamp (ISO 8601), or `null` when none is set.
            At

            read time, once `publish_at` has passed a stored `scheduled` row
            resolves

            to `published` automatically. Models go-live only — there is no

            corresponding unpublish-at timestamp.
          example: null
        seo:
          $ref: '#/components/schemas/Seo'
        metafields:
          type: array
          description: >
            Custom metadata entries (each a `namespace`/`key`/`value` with a

            `value_type`) attached to the category; empty array when none are
            set.
          items:
            $ref: '#/components/schemas/Metafield'
        languages:
          type: array
          items:
            type: string
          description: |
            ISO codes of locales this resource is translated into.
            Always includes the store's default language.
            Always includes the company default locale; never empty.
          example:
            - en
            - fr
            - de
        parent_id:
          type:
            - integer
            - 'null'
          description: >
            The id of the parent category, or `null` when the category is at the
            root

            (top level). The literal `"root"` is a value of the
            `filter[parent_id]`

            query parameter, not a value of this field.
          example: null
        position:
          type: integer
          description: >
            The category's sort order among its siblings. The default catalog
            sort

            (`position` then `id`) uses it.
          example: 1
        source_type:
          type:
            - string
            - 'null'
          description: |
            Which kind of resource this Category groups
            (`product` / `medium` / `page` / `library` / `promotion`).
          example: product
        has_children:
          type: boolean
          description: >
            `true` when the category has at least one child category, counted

            across every lifecycle state — a live parent whose only children are

            draft, scheduled, or archived still reports `true`. It is NOT scoped
            to

            live children, even on the public (live-only) surface. `false` only
            when

            the category is a true leaf. Use it to decide whether to fetch a
            level

            deeper when walking the tree.
          example: false
        countries:
          type: array
          items:
            type: string
          description: |
            ISO codes restricting availability. Empty array means
            available in every country the company sells in
            (unrestricted).
          example:
            - GB
            - US
    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'
    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'
    Seo:
      type: object
      description: >
        Render-ready search-engine optimization metadata. ALWAYS present

        on every storefront response — never null. Values are resolved

        server-side via this priority chain:


        - `title`         SEO title → resource title → company name

        - `description`   SEO desc → resource desc → company default → "Live
        Free."

        - `image_url`     SEO image → resource og_image → resource image →
        company default → hardcoded

        - `block_crawler` SEO block_crawler (false when no SEO row)


        Frontends, AEO crawlers, and SDKs can render directly from these

        values — no client-side fallback logic, no chance of HTML <head>

        and JSON API drifting. `id` is exposed only on the authenticated

        surface and distinguishes a merchant-configured SEO row from

        inherited defaults.
      required:
        - title
        - description
        - image_url
        - block_crawler
      properties:
        title:
          type: string
          example: Summer Sale — Up to 40% Off
          description: Resolved title. Never null.
        description:
          type: string
          example: Shop the July clearance event before it ends.
          description: Resolved description. Never null.
        image_url:
          type: string
          example: >-
            https://ik.imagekit.io/fluidapp/tr:w-1200,h-630,c-maintain_ratio,fo-auto,f-auto,q-80/seo/9215.png
          description: Resolved OG-ready 1200×630 image URL. Never null.
        block_crawler:
          type: boolean
          example: false
          description: When true, the resource page emits a noindex meta tag.
        id:
          type:
            - integer
            - 'null'
          example: 9215
          description: >-
            Authenticated-only. SearchEngineOptimizer row id when one exists;
            null when the values are inherited from the resource / company /
            hardcoded defaults.
    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
    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:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableEntity:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

````