> ## 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, run `fluid signup` from the Fluid CLI, or 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. Before authenticated calls are possible, the merchant must sign the agent in to the Fluid CLI with the emailed code and --shop set to the company's fluid_shop (which also confirms the signup), or open the emailed confirmation link and give the agent an API token.
> 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.
> To host a small project on Fluid (a landing page, event page, calculator or dashboard), follow /sites/quickstart: npm create @fluid-app/fluid-site my-site (or npx @fluid-app/create-fluid-site my-site), fluid login, then fluid deploy prints the public URL. Launch a Site unless the project needs a custom domain, server rendering or heavy server code, which make it an App (a Mist app); /sites/site-or-app has the rule, and POST /api/company/v2026-10/project-launch-recommendations applies it. With only the Fluid MCP server (its Site write tools are limited to Fluid root admins today; see /sites/deploy-from-claude), use site_create, then site_deploy (from a chat with no shell, in one call with the files inline: site_template_get for a starter, then site_deploy with each path's content; or a built folder in two calls: send the manifest, PUT the listed uploads, call again with the bundle_id to get the url), then, only after the person confirms they want it public, site_visibility_set. site_logs, site_traffic, site_env_list/set/unset, site_seo_get/set and the site_branch tools cover logs, traffic, env vars, SEO and preview branches; a member builds their own Sites with the my_site_* tools. The MCP server can't publish server functions or convert a Site to an App, so use the fluid sites CLI for those. A Site can serve light server code: each functions/<name>.ts, declared in config/functions.json with access public, signed_in or key (and an optional UTC cron schedule), answers at /api/<name> on the Site's own host; publish it with fluid deploy (the MCP site_deploy tool does not carry functions). See /sites/functions and /sites/rest-api.
> 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 on an operation whose spec documents those params — mostly the unversioned company-v0 admin surface, plus some lists on other surfaces, versioned ones included; page every other list with cursors.
> 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 except two operations that take no credential, POST /api/company (create a company) and GET /api/agreements/{id} — 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.

# Serve a REST API from a Site

> Build a Site whose server functions serve a REST API end to end: write the functions, declare their access, publish with fluid deploy, add an API key, call it with curl, run a job on a schedule and read the log.

This guide builds a company event Site, `fall-gala`, whose page takes RSVPs from signed-in members and whose REST API lets a partner's server read the guest list. A nightly job removes old RSVPs. Every step uses the Fluid CLI. For the full reference, see [Server functions](https://docs.fluid.app/sites/functions.md).

## Before you start

* Fluid CLI with its Sites plugin 0.1.349 or later, signed in with `fluid login`. See [Publish a Site](https://docs.fluid.app/sites/publishing.md#before-you-start).
* `@fluid-app/sites` 0.1.71 or later in the Site's `package.json`, for `fluid.fn`.
* A Site folder, from `npm create @fluid-app/fluid-site fall-gala` or an existing project. See the [Sites quickstart](https://docs.fluid.app/sites/quickstart.md).
* Sites and the sites gateway turned on for your company. Until then, the routes for API keys, environment variables and logs answer `404`. To turn them on, [contact support](https://docs.fluid.app/help/getting-help.md).

**1. Add a table for the data**

Functions read and write the Site's own database. Add the table to `db/schema.json`. See [the site database](https://docs.fluid.app/sites/database.md) for the format and its rules.

```json db/schema.json theme={null}
{
  "version": 1,
  "tables": {
    "rsvps": {
      "columns": {
        "event_id": { "type": "text", "required": true },
        "guests": { "type": "integer", "required": true },
        "member_name": { "type": "text", "required": true }
      },
      "rules": { "read": "owner", "insert": "nobody", "update": "nobody", "delete": "nobody" }
    }
  }
}
```

The rules keep the page from writing the table directly. Functions query as the Site, so the rules don't apply to them, and the `rsvp` function below is the only way in.

**2. Write the functions**

Each file at the top of `functions/` is one function, named after the file.

```ts functions/rsvp.ts theme={null}
// POST /api/rsvp, from the Site's page, for a signed-in member.
export default async function (request: Request, ctx: any): Promise<Response> {
  if (request.method !== "POST") {
    return Response.json({ error: "method_not_allowed" }, { status: 405 });
  }
  const viewer = ctx.caller.kind === "page" ? ctx.caller.viewer : null;
  if (viewer === null) return Response.json({ error: "sign_in_required" }, { status: 401 });

  const { eventId, guests } = await request.json();
  if (typeof eventId !== "string" || !Number.isInteger(guests) || guests < 1 || guests > 4) {
    return Response.json({ error: "invalid", message: "guests must be 1 to 4" }, { status: 422 });
  }
  const { data, error } = await ctx.fluid.db
    .from("rsvps")
    .insert({ event_id: eventId, guests, member_name: viewer.name })
    .select();
  if (error) return Response.json({ error: error.code }, { status: 400 });
  return Response.json({ rsvp: data[0] }, { status: 201 });
}
```

```ts functions/guests.ts theme={null}
// GET /api/guests/<event-id>, for a partner's server with a Site API key.
export default async function (request: Request, ctx: any): Promise<Response> {
  if (request.method !== "GET") {
    return Response.json({ error: "method_not_allowed" }, { status: 405 });
  }
  const [, , , eventId] = new URL(request.url).pathname.split("/"); // /api/guests/<event-id>
  if (!eventId) return Response.json({ error: "not_found" }, { status: 404 });

  const { data, error } = await ctx.fluid.db
    .from("rsvps")
    .select("member_name, guests, created_at")
    .eq("event_id", eventId)
    .order("created_at")
    .limit(500);
  if (error) return Response.json({ error: error.code }, { status: 400 });
  return Response.json({ event_id: eventId, guests: data });
}
```

```ts functions/nightly-cleanup.ts theme={null}
// Runs every night at 06:00 UTC: removes RSVPs older than a year.
export default async function (_request: Request, ctx: any): Promise<Response> {
  // Any key opens every `key` function, so refuse callers other than the schedule.
  if (ctx.caller.kind !== "schedule") {
    return Response.json({ error: "forbidden" }, { status: 403 });
  }
  const cutoff = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString();
  const { error } = await ctx.fluid.db.from("rsvps").delete().lt("created_at", cutoff);
  return Response.json({ ok: error === null }, { status: error ? 500 : 200 });
}
```

Functions run on Cloudflare Workers, not Node.js. Use web APIs such as `fetch` and `crypto.subtle`, not `node:` modules.

**3. Declare who may call each one**

```json config/functions.json theme={null}
{
  "version": 1,
  "functions": {
    "rsvp": { "access": "signed_in" },
    "guests": { "access": "key" },
    "nightly-cleanup": { "access": "key", "schedule": "0 6 * * *" }
  }
}
```

`signed_in` lets only a signed-in visitor call from the Site's page. `key` needs one of the Site's API keys. Giving the scheduled function `key` access keeps guests from calling it, and its check of `ctx.caller.kind` keeps a key holder, such as the partner, from running it.

**4. Call the function from the page**

```ts src/rsvp.ts theme={null}
import { fluid } from "@fluid-app/sites";

export async function sendRsvp(guests: number) {
  const res = await fluid.fn.fetch("rsvp", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ eventId: "fall-gala", guests }),
  });
  if (res.status === 401) return fluid.auth.signIn();
  if (!res.ok) throw new Error((await res.json()).error);
  return (await res.json()).rsvp;
}
```

Use `fluid.fn.fetch`, not the page's own `fetch`: it sends the visitor's sign-in with the call.

**5. Check and publish**

```bash theme={null}
npm run build
fluid sites lint
fluid deploy
```

`fluid sites lint` checks the build output, so build first. It also builds each function and checks `config/functions.json` without a request. `fluid deploy` builds the Site, publishes the page, the schema and the functions together, and prints the Site's address. A new Site is private, so `rsvp` answers only the people it's shared with until you share it or make it public. Ask before you make it public. The partner's key works either way.

**6. Store any secrets**

If a function calls another service, keep its key as a secret. Functions read it from `ctx.secrets`, and a change reaches them without a new publish:

```bash theme={null}
printf %s "$PARTNER_WEBHOOK_SECRET" | fluid sites env set PARTNER_WEBHOOK_SECRET --secret --stdin
```

**7. Add an API key for the partner**

With Sites plugin 0.1.360 or later, run `fluid sites keys create partner` in the Site's folder: it prints the secret once. With an older plugin, call the API. For a company Site, use a signed-in admin's token and the `site_id` in the folder's `.member-site.json`:

```bash theme={null}
curl -X POST "https://api.fluid.app/api/company/v2026-10/member-sites/$SITE_ID/api-keys" \
  -H "Authorization: Bearer $FLUID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Partner guest list"}'
```

The answer's `api_key.secret`, which starts with `fsk_`, is shown only this once. For a member's own Site, call `POST /api/member/v2026-10/sites/$SITE_ID/api-keys` with the member's token instead: its answer carries the secret at `data.secret`. Give it to the partner over a private channel, and never put it in the Site's code. See [Add an API key to a company site](https://docs.fluid.app/api-reference/member-site-api-keys/add-an-api-key-to-a-company-site.md).

**8. Call the API**

The function answers at the Site's own host, `https://<label>.sites.fluid.app`. The label is normally the company's subdomain and the Site's handle, joined by a hyphen. To be sure, read `window.location.origin` on the Site's page.

```bash theme={null}
curl "https://primex-fall-gala.sites.fluid.app/api/guests/fall-gala" \
  -H "Authorization: Bearer $FALL_GALA_API_KEY"
```

```json theme={null}
{
  "event_id": "fall-gala",
  "guests": [
    { "member_name": "Riley Kim", "guests": 2, "created_at": "2026-11-10T18:04:11.212Z" }
  ]
}
```

A key opens the Site's `key` and `public` functions even while the Site is private. Without the key, the call answers `401` `fn_key_required`.

**9. Read the log**

```bash theme={null}
fluid sites logs --since 1h
fluid sites traffic --period 7d
```

Each call is one line with its function, caller, status and time, and the nightly run shows up as a `schedule` call. What a function prints with `console.log` isn't in the log.

## Rotate or revoke a key

To rotate, add a new key, move the partner to it, then [revoke](https://docs.fluid.app/api-reference/member-site-api-keys/revoke-a-company-sites-api-key.md) the old one by its id. [List the keys](https://docs.fluid.app/api-reference/member-site-api-keys/list-a-company-sites-api-keys.md) to find the id: each shows its name and first 12 characters, never its secret. A revoked key stops working on the next call.

## When something fails

| Answer | What it means |
| - | - |
| `fluid sites lint` names a file in `functions/` | A function file isn't in `config/functions.json`, has no default export, imports a Node module, or is over 10 MiB |
| `422` `functions_api_path_taken` on publish | The build has a file under `/api/`, where functions answer. Move it |
| `401` `fn_api_needs_viewer` inside a function | `ctx.fluid.api` was used on a call with no signed-in visitor, such as a key or schedule call |
| `500` `fn_error` | The handler threw. The answer and the log line carry the request id |

The full list is in [What the answers mean](https://docs.fluid.app/sites/functions.md#what-the-answers-mean).


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