> ## 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.
> To sign a new merchant up for Fluid programmatically, call POST https://api.fluid.app/api/company with no credentials, as described in /api/agent-signup. Do not automate the signup form or book a demo. The merchant must open the emailed sign-in link and create an API token before authenticated calls are possible.
> After signup, to launch a merchant's store (back-office sync, payments onboarding, store setup, a theme copied from their current site, and a preview link before billing), follow /api/agent-launch.
> Lists use cursor pagination via the page[cursor] and page[limit] query params; follow meta.pagination.next_cursor until it is null.
> When the same operation exists on more than one surface, use the newest: dated API versions are newer than unversioned ones, and later dates win (v2026-04 > v2025-06 > unversioned v0/v1.1). Fall back to a legacy or unversioned operation only when no newer versioned equivalent exists — the company-v0 notes below list the known superseded operations. The same applies to /api/company/v1 and /api/v1/... paths: prefer a newer documented equivalent, and use one only when none exists (/api/v1.1/... is distinct and documented in company-v0). Use page/per_page offset pagination only where a spec documents it — in practice the unversioned company-v0 admin surface; every versioned surface uses cursor pagination.
> Fluid has three navigation APIs; don't mix them up. Storefront website menus (navigation bars, footers) are /api/menus and nested menu_items, in api-reference/content-v0.yaml (API Reference: Website > Navigation menus), with a how-to in themes/navigation-menus; their list uses flat page/per_page pagination. The Fluid mobile app's navigation is /api/v2/mobile_navigations, in api-reference/mobile-v2.yaml (API Reference: Mobile app > Navigation); its list also uses page/per_page. Portal navigations belong to a portal definition (Fluid OS), in api-reference/fluid-os-v0.yaml (API Reference: Portal > Portal navigation), and each has a platform of web or mobile.
> 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/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); api-reference/company-v0.yaml covers the legacy unversioned company admin surface (/api/... paths, bearer-authenticated — company settings and management, customers, users, roles, subscription plans, subscription bundles, subscriptions, media, pages, catch-ups, inventory levels, domains, agreements, and admin order actions). company-v0 caveats: it is the legacy v0 admin contract and its lists use flat page/per_page offset pagination, which is expected there despite the general cursor-pagination rule; where an operation exists in both company-v0 and a versioned spec, prefer the versioned spec — the subscriptions lifecycle (list/create/show/update, cancel, pause, reactivate, resume, retry, skip, failed-cycle-waiver, discounts) and subscription bundles are superseded by checkout-v2026-04, and company pages/media CRUD plus the public pages, categories, products, and media list endpoints are superseded by storefront-v2026-04. Subscription plan management (/api/subscription_plans, resource-wrapped {"subscription_plan": {...}} bodies) exists only in company-v0. api-reference/members-v2025-06.yaml covers the v2025-06 unified Member identity surface (/api/v2025-06/members/... paths, bearer-authenticated — member list/create/show/update, lookup by email/username/external_id/legacy_customer_id, member-type assignment, and the sponsor genealogy read). Prefer it over the customers and reps surfaces when the member type matters: /customers does not serialize member_type. api-reference/analytics-v2026-04.yaml covers the v2026-04 Home dashboard analytics surface (/api/v202604/analytics/dashboard/... paths, bearer-authenticated — read-only endpoints for the Home > Overview, Home > Live, and Home > Field tabs, each accepting an optional country ISO alpha-2 query param that scopes aggregations to a single country).
> api-reference/analytics-v0.yaml covers the unversioned analytics surface that backs the fluid-admin Traffic tab (/api/analytics/... and /api/analytics/traffic/... paths, bearer-authenticated — the legacy shares/views/visitors summary plus traffic overview, ranked campaigns, sources, geographies, flows, and per-rep breakdown, all sharing one reporting-period contract).
> Successful responses wrap the resource payload alongside a top-level integer status and a meta object.
> Portal Definition authoring edits and synchronizes the portal JSON resource graph. Widget Package authoring builds either a company-owned or Droplet-owned Remote DOM package. These are separate contracts; do not imply that one defines the other.
> For Widget Package worker code, use only @fluid-app/portal-sdk/widgets/worker. Use only the Portal Definition and Widget Package workflows and public entry points documented here; do not infer support for undocumented surfaces.
> Every portal function and declarative capability used by a widget must appear in that widget's uses array. Use the same typed function value in uses; do not invent capability-name strings.
> Widget styling must use the portal's semantic theme variables for colors, typography, spacing, radii, borders, focus, and charts whenever a token represents the visual decision. Do not create a separate light or dark palette or duplicate theme controls as widget properties.
> Prefer worker-safe Fluid UI components exported by @fluid-app/portal-sdk/widgets/worker when they fit the interaction. When no exported component fits, use semantic HTML, accessible behavior, and the portal theme variables.
> A Portal Definition push updates the remote working definition. A portal version is an immutable snapshot, and activation is a separate live release operation.
> The Help Center (/help/...) is for merchants, admins and reps using Fluid. Its admin pages mirror the admin's routes: the screen at admin.fluid.app/settings/taxes is documented at /help/admin/settings/taxes. Use the Help Center for how-to questions about the admin, and the Developer Platform and API Reference for building integrations.
> Help Center pages describe what a company admin sees. A reader's role can hide screens and actions; admins manage roles on Settings > Roles (/help/admin/settings/roles). If someone can't find a screen or button, their role's permissions are the first thing to check.
> Send people who need Fluid support to /help/getting-help. Don't invent support email addresses, phone numbers or response times.

# Build a custom cart drawer

> Keep your own cart sidebar or your own add button in a Fluid theme: open it at the click, draw pending lines, and handle a fill that fails.

Every Fluid storefront creates carts in the background. The first add on a new cart creates the cart, and the server fills it in. For a moment the cart is empty and its `population_status` is `pending`. The fill usually takes a few hundred milliseconds to about 2 seconds. For the cart methods and their return values, see [Carts that fill in the background](/sdk/cart-api#carts-that-fill-in-the-background).

The SDK cart drawer hides this wait. It opens at the click and draws the line, title, image and price at once, then reconciles with the server's cart when the fill finishes. The SDK draws that line from the add and from a map of line previews that Fluid puts on every storefront page.

Pick the path that matches your theme:

| Your theme | What to do |
| - | - |
| Declarative buttons only: `data-fluid-add-to-cart`, `data-fluid-add-enrollment-pack`, or `{% form 'product' %}` | Nothing. The SDK opens the drawer for you. |
| Custom add button, quick view or bundle builder that calls `addCartItems()`, with the SDK drawer | [Open the SDK drawer at the click](#keep-the-sdk-drawer-with-a-custom-add-button). |
| Your own cart sidebar | [Build your own drawer](#build-your-own-drawer). |

## Build your own drawer

A theme with its own cart sidebar draws the lines itself. While a new cart fills, the server's cart has no items. The SDK keeps the lines of adds the server has not answered yet, so your sidebar can draw them in the meantime.

1. **Open your drawer at the click.** Do not wait for the server. Open it from your own click handler, or from the `CART_ITEMS_ADDING` window event, which fires synchronously before any request. On an SDK button, set `data-fluid-open-cart-after-add="false"` so the Fluid cart does not open as well.
2. **Render the cart's lines, then the pending lines.** Read the stored cart with `getLocalCart()` and the pending lines with `getPendingCartItems()`.
3. **Render again on every `onCartChange` callback.** The handler is not called when you subscribe, so render once yourself first.
4. **Show the subtotal as calculating while items are pending.** Fields such as `cart.sub_total` do not count pending lines yet.
5. **Check out with `FairShareSDK.checkout()`.** It waits for the fill before it redirects. A page that redirects on its own calls `await FairShareSDK.waitForCheckoutReady()` first, or it checks out a cart that is still filling.
6. **Handle `CART_POPULATION_FAILED`.** If the server cannot fill the cart, show the reason and let the shopper try again.

### Read the pending lines

`getPendingCartItems()` returns the unsaved lines, newest last. Each item is the line as the add announced it: `variant_id`, `quantity`, `subscription`, `subscription_plan_id`, and, when the page gave them, a `preview` with `product_title`, `variant_title`, `image_url` and the prices. Bundle lines also carry `bundled_items`. The SDK adds these fields:

| Field | Meaning |
| - | - |
| `operationId` | The add the line belongs to. |
| `cartToken` | The cart the add targets. It is `null` while the add that creates the cart runs. |
| `quantity` | The quantity this add puts on the line, not the line's total. A second add of a line the cart already lists has a `quantity` of 1. |
| `listedQuantity` | What the stored cart listed for this line when the add was announced. It is 0 when the cart lists none. |

The line holds `listedQuantity + quantity` once the add lands. So:

* A pending line for a variant already in the cart shows the listed quantity plus the pending quantity, on the same row.
* A pending line for a variant that is not in the cart is its own row, with the pending `quantity`.

Match a pending item to a cart line on its variant and its subscription state, never on the variant alone. A subscription line and a one-time line of the same variant are two cart lines. Treat a cart line whose `subscription` is `null` as one-time. A server cart lists the variant as `item.variant.id`, not as a top-level `variant_id`.

For a bundle, the child selection is part of the match. Two bundle adds of one parent variant with different child selections are two lines. Match them with `bundleConfigMatches(item, pendingItem.bundled_items)`, which `window.FairShareSDK` exports. Two adds of one line in flight are two items: add their `quantity` values together.

An enrollment add's lines carry `enrollment: true`. Draw them without a price, because enrollment moves the cart to member pricing. Don't estimate a total while one is pending.

A subscription line can carry `subscription_plan`, such as `{ billing_interval: 4, billing_interval_unit: "week" }`. Use it to write "Every 4 weeks" on the pending row. It is absent on a one-time line, on an enrollment line, and when the page's map has no entry for the plan. Draw the row without an interval then.

The SDK stops returning a pending item when:

* A cart body lists its line at `listedQuantity + quantity` or more.
* Its add fails.
* On a new cart, its add has settled and a cart body for that cart is no longer `pending`.
* The cart's background fill fails (`CART_POPULATION_FAILED`), or the SDK discards the cart.
* 35 seconds have passed. After that, the server's cart is all there is.

The returned items are copies. Changing them changes nothing.

### Subscribe with `onCartChange`

`onCartChange(handler)` calls `handler({ cart, pendingItems })` after every change to the pending items or to the stored cart. That covers an add starting or settling, a cart arriving from the server, a failed fill, a discarded cart, and an item expiring. It returns a function that unsubscribes.

* `cart` is the latest cart the SDK stored from the server. It is `null` after the cart is cleared. The SDK never merges it with the pending items. You combine them.
* `pendingItems` is what `getPendingCartItems()` returns at that moment.
* A handler that throws is logged. It does not stop other handlers or the SDK.

### Complete example

This example adds a button, a drawer with a list, a total and a checkout button. It opens the drawer at the click, draws the cart's lines and the pending lines, shows the total as calculating while items are pending, and reports a failed fill. Put it in a section's Liquid file.

```liquid theme={null}
<button
  data-fluid-add-to-cart="{{ product.selected_or_first_available_variant.id }}"
  data-fluid-open-cart-after-add="false"
>
  Add to cart
</button>

<aside id="juniper-cart" hidden>
  <button id="juniper-cart-close" type="button">Close</button>
  <ul id="juniper-cart-lines"></ul>
  <p id="juniper-cart-notice" role="alert"></p>
  <p id="juniper-cart-total"></p>
  <button id="juniper-cart-checkout" type="button">Checkout</button>
</aside>

<script>
  // The SDK loads as a module script, so wait for the page to finish parsing.
  window.addEventListener("DOMContentLoaded", () => {
    const sdk = window.FairShareSDK;
    const drawer = document.getElementById("juniper-cart");
    const list = document.getElementById("juniper-cart-lines");
    const notice = document.getElementById("juniper-cart-notice");
    const total = document.getElementById("juniper-cart-total");

    function render({ cart, pendingItems }) {
      const rows = (cart?.items ?? []).map((item) => ({
        item,
        base: item.quantity,
        added: 0,
        title: item.product_title,
      }));

      for (const pending of pendingItems) {
        // The same line: variant, subscription state and, for a bundle,
        // its child selection.
        let row = rows.find(
          ({ item }) =>
            item.variant?.id === pending.variant_id &&
            !!item.subscription === !!pending.subscription &&
            sdk.bundleConfigMatches(item, pending.bundled_items),
        );
        if (!row) {
          row = {
            item: {
              variant: { id: pending.variant_id },
              subscription: pending.subscription,
              bundled_items: pending.bundled_items,
            },
            base: 0,
            added: 0,
            title: pending.preview?.product_title ?? "Adding...",
            pending: true,
          };
          rows.push(row);
        } else if (!row.added) {
          row.base = pending.listedQuantity;
        }
        // Two adds of one line are one row.
        row.added += pending.quantity;
      }

      list.replaceChildren(
        ...rows.map((row) =>
          Object.assign(document.createElement("li"), {
            textContent: `${row.base + row.added} x ${row.title}${row.pending ? " (adding)" : ""}`,
          }),
        ),
      );
      total.textContent = pendingItems.length
        ? "Calculating total..."
        : (cart?.sub_total_in_currency ?? "");
    }

    // Open at the click: this event fires before any request.
    window.addEventListener("CART_ITEMS_ADDING", () => {
      notice.textContent = "";
      drawer.hidden = false;
    });

    // The server could not fill the cart. The SDK has already dropped the
    // pending lines and calls onCartChange, so the list renders again.
    window.addEventListener("CART_POPULATION_FAILED", (event) => {
      notice.textContent =
        event.detail.apiMessage ?? "We couldn't add those items. Try again.";
    });

    sdk.onCartChange(render);
    render({ cart: sdk.getLocalCart(), pendingItems: sdk.getPendingCartItems() });

    document.getElementById("juniper-cart-close").onclick = () => {
      drawer.hidden = true;
    };
    document.getElementById("juniper-cart-checkout").onclick = async () => {
      try {
        await sdk.checkout(); // Waits for the fill, then redirects.
      } catch (error) {
        notice.textContent = "We couldn't start checkout. Try again.";
      }
    };
  });
</script>
```

A bundled theme can import `getPendingCartItems`, `onCartChange` and `bundleConfigMatches` from `@fluid-app/fluid` instead of reading them from `window.FairShareSDK`.

### Handle a failed fill

When the server cannot fill a new cart, for example because none of the items are sold in the shopper's country, the call that created the cart resolves `undefined` and no `CART_OPERATION_SUCCESS` fires. The SDK emits `CART_POPULATION_FAILED` with `cartToken`, `errorMessage`, `errors`, and `apiMessage` when the server gave a reason a shopper can read. Show `apiMessage` and never `errorMessage`, which is often a bare status phrase. See [Cart population failures](/sdk/cart-api#cart-population-failures).

## Keep the SDK drawer with a custom add button

Declarative buttons need no theme code. If your theme calls `addCartItems()` itself, for a custom button, a quick view or a bundle builder, and you want the SDK drawer, open the drawer at the click. Call `FairShareSDK.cart.control.open()` before you await the add. The drawer then shows the pending line, with its title, image and price, the moment the shopper clicks.

```javascript theme={null}
buyButton.addEventListener("click", (event) => {
  // Open at the click, before the add starts.
  window.FairShareSDK.cart.control.open();

  window.FairShareSDK.withButtonLoading(event.currentTarget, () =>
    window.FairShareSDK.addCartItems(48291, { quantity: 1 }),
  );
});
```

`addCartItems()` does not open the drawer by itself. Don't call `open()` after the add resolves: on a new cart the add waits for the fill, a few hundred milliseconds to about 2 seconds, so the shopper would see nothing change after the click.

`withButtonLoading()` keeps the button's spinner on until the cart is filled. See [Configure cart feedback](/themes/cart-feedback).

`addCartItems()` resolves with the filled cart, so use that value when you need the items. Don't read them with `getLocalCart()` straight after an add you did not await. `CART_ITEMS_SETTLED` can carry a cart that is still filling, so code that needs the final items should use the value the add resolved with, [`whenCartSettled()`](/sdk/cart-api#carts-that-fill-in-the-background), or `onCartChange()`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.