Skip to main content
A member site’s templates carry the site’s structure and menus. They don’t carry the viewer’s data. A site gets that data in its own JavaScript, through window.fluid. It has two members: fluid.fetch, which reaches Fluid’s APIs, and fluid.viewer(), which says who is viewing. This page covers both, who gets data back, how to handle each answer and how to declare what your scripts use.

Know how a script reaches Fluid

A site renders in a sandboxed frame. The frame has an opaque origin and no cookie. So a call from your script with the page’s own fetch carries no session and can’t reach a Fluid API as the viewer. The site SDK installs window.fluid in the frame instead. A call travels from the frame to Fluid’s page around it, and from there to the member API as the viewer. Fluid forwards only routes it allows. Site management routes, such as a site’s files and pages, aren’t reachable from site code.
  • fluid.fetch(path, init) behaves like fetch. It takes the same arguments and resolves to a Response.
  • fluid.viewer() says who is viewing. It returns "member" or "guest".
  • The page’s own fetch stays as it is. Fluid never replaces window.fetch, so you can still use it to call other services. It doesn’t reach Fluid.
The SDK loads through content_for_header, so print it in your layout’s <head>. Load your script with {{ 'site.js' | asset_url | script_tag }}, or write it inline. See Use tags and filters.
Never put a member’s data into Liquid output or a <script> through a template variable. Never call a Fluid API with the page’s own fetch, XMLHttpRequest or a form post from a site. The frame has no session, so those calls fail.

Read with fluid.fetch

Pass a path on your company’s own host. A Member API path, such as /api/member/v2026-10/me, goes as it is. Check response.ok, then read the body:
assets/site.js
The page prints an empty element for the script to fill:
pages/index.liquid
Render the structure in Liquid and fill the viewer’s data in from the script. Show a neutral fallback while a call is pending and when it fails. See Show the signed-in member for the body.

Write with fluid.fetch

Set the method in init, as you would with fetch. Send a JSON body with a content-type header:
assets/site.js
Fluid forwards GET, POST, PUT, PATCH and DELETE. It answers any other method itself, with a 403. A request body must be JSON, and Fluid refuses one that is too large. See Update the signed-in member for the fields.

Choose the path

  • A same-host path under /api/ works. A full URL on your company’s own origin works too.
  • A Member API path stays. A path that starts with /api/member/v2026-10/ goes as you wrote it.
  • Any other /api/ path moves. Fluid sends /api/<x> to /api/member-storefront/<x>, the company host’s bridge routes.
  • Anything else throws. A URL on another host, or a path that doesn’t start with /api/, throws an Error before anything is sent.

Know who gets data

Only the signed-in owner of a member-owned site gets data from fluid.fetch, and only while viewing the site published. A site whose published declaration lists no scope gets none, even for its owner.
  • Sharing grants nothing. Sharing a site or making it public doesn’t change who gets data. A share holder, a company admin and a visitor never get a scope from the site’s declaration. See Share a member site.
  • fluid.viewer() follows that. It returns "member" only when the site’s frame can reach Fluid for the viewer. A signed-in viewer the frame can’t act for gets "guest", and a call answers with the local guest response below.
  • A company site can’t use viewer data. If you want a script to read the viewer, build a member-owned site. Build a company site from Liquid’s structure and menus only.
  • A script that works for you may fail for a visitor. Test with a second member, and show a fallback when a call fails.
  • A site whose owner is no longer an active member grants no scope to anyone. Its calls are refused.
  • A change to the site applies at once. A take-down, an owner who is no longer active and a publish that drops a scope all refuse the next call. Fluid checks the site on every request.

Handle the Response

fluid.fetch resolves to a Response for any HTTP status, so a refusal isn’t an exception. Check response.ok first. Fluid’s own page answers some requests itself, with a JSON body that holds an error string: Any other status comes from the API, with its own body. One refusal is about the site’s declaration. The owner of a site that published a declaration gets a 403 for a path whose scope the declaration lacks. The body names the scope:
Every other refusal of a bridged call keeps one body, member_site_unavailable, so a response never says whether a site exists. That covers a site that is unknown, unpublished or company-owned, a viewer who isn’t its owner and a path that no scope covers. Read the status first, then the body:
assets/site.js
A call rejects only when something outside the API goes wrong:
  • An Error means the path is on another host or outside /api/. Nothing is sent.
  • A TypeError means the transport failed. Fluid’s page couldn’t be reached, timed out, or answered with a network error.
  • An AbortError means the request’s signal aborted.
Wrap a call in try and catch so a rejection keeps the page usable.

Use a library

A library that accepts a custom fetch works with fluid.fetch. Pass it in when you create the client. For example, with ky:
assets/site.js
A library built on XMLHttpRequest doesn’t go through fluid.fetch, so it can’t reach Fluid from a site. A library that calls the global fetch directly has the same limit.

Know the scope of a path

Each path has a scope. A scope is <family>: read or <family>: write. The method picks the mode: GET needs read, and any other method that Fluid forwards needs write. The route picks the family, so a request can’t borrow another family’s scope by changing its path. See Show the signed-in member, Update the signed-in member, List the member’s tasks and Complete a task. The API reference lists the rest of the member API. When you aren’t sure which scope a path needs, call it from a script and run fluid member-site lint, which names the scope the file lacks. Some routes have no scope. A site can never reach them, and no declaration changes that. Saved payment methods and site management are examples, and so are writes to addresses and subscriptions. A call to one is refused whatever the site declares.

Declare the scopes you use

List the scope of each path your scripts call in config/access.json. The file is one JSON object with one key, uses:
config/access.json
fluid member-site lint checks each fluid.fetch(path, { method }) call in your scripts against the scope of that path. It warns when the file lacks the scope, when the route can never be reached by a site, and when it can’t read the call. Run it before you push. A site with no config/access.json isn’t checked this way. The file is published with the site. Fluid enforces the published declaration. It refuses a call whose scope the file lacks with the member_site_scope_undeclared response above. Declare what a member site uses lists every scope you can declare and explains how to write, publish and check the file.

Keep scripts working with the published site

A script in assets/ is live as soon as you push it, while page content stays a draft until you publish. Write a script that still works with the published pages. For example, a script that looks for [data-greeting] and does nothing when it isn’t there works before and after the page that prints it is published.