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

# Latest Lighthouse result

> Returns the most recent successful Lighthouse scan for the Enrollment
Pack (scans persist only on success). A fresh scan is auto-queued on
every company create and update, but it runs asynchronously — a
failed, rate-limited, or recently-run rescan writes no new record,
so this endpoint may return the prior scan with HTTP 200. `POST
.../lighthouse` requests a rescan (it always returns 202) but
cannot guarantee a newer score — the queued scan is skipped
silently when one ran in the last ~5 minutes, the scanner is
rate-limited, or its circuit breaker is open.




## OpenAPI

````yaml /api-reference/storefront-v2026-04.yaml get /api/v202604/company/enrollment-packs/{id}/lighthouse
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/enrollment-packs/{id}/lighthouse:
    get:
      tags:
        - company
      summary: Latest Lighthouse result
      description: |
        Returns the most recent successful Lighthouse scan for the Enrollment
        Pack (scans persist only on success). A fresh scan is auto-queued on
        every company create and update, but it runs asynchronously — a
        failed, rate-limited, or recently-run rescan writes no new record,
        so this endpoint may return the prior scan with HTTP 200. `POST
        .../lighthouse` requests a rescan (it always returns 202) but
        cannot guarantee a newer score — the queued scan is skipped
        silently when one ran in the last ~5 minutes, the scanner is
        rate-limited, or its circuit breaker is open.
      operationId: companyEnrollmentPacksLighthouseShow
      parameters:
        - name: id
          description: The unique identifier of the enrollment pack.
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Latest Lighthouse result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LighthouseResult'
        '202':
          description: |
            No Lighthouse scan existed yet; one was triggered
            automatically. Poll `GET :id/lighthouse` for the result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LighthouseScanTriggered'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - bearer_auth: []
components:
  schemas:
    LighthouseResult:
      type: object
      description: |
        Latest Lighthouse result for the resource, returned raw — no
        `meta`, no top-level `status`. `core_metrics`, `category_scores`,
        and
        `optimization_opportunities` are derived from the raw `result`
        blob; any derived value can be `null` when the run did not
        produce it.
      required:
        - id
        - scanned_at
        - created_at
        - updated_at
        - device_type
        - final_url
        - fetch_time
        - core_metrics
        - category_scores
        - optimization_opportunities
        - result
      properties:
        id:
          type: integer
          example: 55
        scanned_at:
          type:
            - string
            - 'null'
          format: date-time
          example: '2026-06-23T13:42:17Z'
        created_at:
          type: string
          format: date-time
          example: '2026-06-23T13:42:17Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-23T13:42:20Z'
        device_type:
          type:
            - string
            - 'null'
          enum:
            - mobile
            - desktop
            - null
          description: Lighthouse form factor (`formFactor` / `emulatedFormFactor`).
          example: mobile
        final_url:
          type:
            - string
            - 'null'
          example: https://acme.fluid.app/home/categories/summer-sale
        fetch_time:
          type:
            - string
            - 'null'
          format: date-time
          example: '2026-06-23T13:42:17Z'
        core_metrics:
          type: object
          description: |
            Summarized metrics derived from the raw result. Every key is
            always present; every value is nullable. Rating fields are
            `good` / `needs-improvement` / `poor` (or `null`).
          required:
            - performance_score
            - largest_contentful_paint
            - largest_contentful_paint_rating
            - first_input_delay
            - first_input_delay_rating
            - cumulative_layout_shift
            - cumulative_layout_shift_rating
            - time_to_interactive
            - total_blocking_time
            - first_contentful_paint
            - speed_index
            - device_type
          properties:
            performance_score:
              type:
                - number
                - 'null'
            largest_contentful_paint:
              type:
                - number
                - 'null'
            largest_contentful_paint_rating:
              type:
                - string
                - 'null'
            first_input_delay:
              type:
                - number
                - 'null'
            first_input_delay_rating:
              type:
                - string
                - 'null'
            cumulative_layout_shift:
              type:
                - number
                - 'null'
            cumulative_layout_shift_rating:
              type:
                - string
                - 'null'
            time_to_interactive:
              type:
                - number
                - 'null'
            total_blocking_time:
              type:
                - number
                - 'null'
            first_contentful_paint:
              type:
                - number
                - 'null'
            speed_index:
              type:
                - number
                - 'null'
            device_type:
              type:
                - string
                - 'null'
        category_scores:
          type: object
          description: |
            Per-Lighthouse-category score, 0–100 (`(score * 100).round`)
            or `null`. Keys are whatever categories the run produced
            (e.g. performance, accessibility, best-practices, seo, pwa).
          additionalProperties:
            type:
              - integer
              - 'null'
          example:
            performance: 91
            accessibility: 96
            best-practices: 92
            seo: 100
            pwa: 70
        optimization_opportunities:
          type: array
          description: |
            Audits with a savings opportunity (score < 1), sorted by
            estimated savings descending.
          items:
            type: object
            required:
              - id
              - title
            properties:
              id:
                type: string
                example: uses-optimized-images
              title:
                type:
                  - string
                  - 'null'
                example: Efficiently encode images
              description:
                type:
                  - string
                  - 'null'
              score:
                type:
                  - number
                  - 'null'
                example: 0.72
              display_value:
                type:
                  - string
                  - 'null'
                example: Potential savings of 120 KiB
              savings_ms:
                type:
                  - number
                  - 'null'
                example: 340
              savings_bytes:
                type:
                  - number
                  - 'null'
                example: 122880
        result:
          type: object
          additionalProperties: true
          description: |
            Raw Lighthouse JSON blob (the upstream Google Lighthouse
            schema). Large (~100 KB typical); the summarized `core_metrics`,
            `category_scores`, and `optimization_opportunities` above are
            derived from it.
      example:
        id: 55
        scanned_at: '2026-06-23T13:42:17Z'
        created_at: '2026-06-23T13:42:17Z'
        updated_at: '2026-06-23T13:42:20Z'
        device_type: mobile
        final_url: https://acme.fluid.app/home/categories/summer-sale
        fetch_time: '2026-06-23T13:42:17Z'
        core_metrics:
          performance_score: 0.91
          largest_contentful_paint: 1850
          largest_contentful_paint_rating: good
          first_input_delay: null
          first_input_delay_rating: null
          cumulative_layout_shift: 0.03
          cumulative_layout_shift_rating: good
          time_to_interactive: 2400
          total_blocking_time: 120
          first_contentful_paint: 1200
          speed_index: 2100
          device_type: mobile
        category_scores:
          performance: 91
          accessibility: 96
          best-practices: 92
          seo: 100
          pwa: 70
        optimization_opportunities:
          - id: uses-optimized-images
            title: Efficiently encode images
            description: Optimized images load faster and consume less cellular data.
            score: 0.72
            display_value: Potential savings of 120 KiB
            savings_ms: 340
            savings_bytes: 122880
        result:
          lighthouseVersion: 11.4.0
          finalDisplayedUrl: https://acme.fluid.app/home/categories/summer-sale
    LighthouseScanTriggered:
      type: object
      description: |
        `GET :id/lighthouse` 202 — returned when no Lighthouse scan exists
        yet, so one is auto-triggered. The body is raw — there is NO `meta`
        envelope.
      required:
        - message
        - scan_status
      properties:
        message:
          type: string
          example: No Lighthouse scan exists. Scan has been triggered automatically.
        scan_status:
          $ref: '#/components/schemas/ScanStatus'
      example:
        message: No Lighthouse scan exists. Scan has been triggered automatically.
        scan_status:
          status: pending
          requested_at: '2026-06-23T13:42:00Z'
    ScanStatus:
      type: object
      description: |
        Progress of an async lighthouse / compliance scan. Only `status`
        is guaranteed (`requested_at` is present in practice once a scan
        is requested); nil timestamps are omitted, so the present keys
        vary by phase — a failed scan carries `failed_at` + `error`, a
        completed one carries `started_at` + `completed_at`.
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - pending
            - in_progress
            - completed
            - failed
          example: pending
        requested_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        failed_at:
          type: string
          format: date-time
        error:
          type: string
      example:
        status: pending
        requested_at: '2026-06-23T13:42:00Z'
    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'
    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'
    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'
  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'
    NotFound:
      description: Resource not found
      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: []`).

````