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

# Exigo connection SDK

> Call Exigo's REST API and query its SQL replica from a droplet or Mist app, with one credential interface and typed errors.

The Exigo connection SDK gives droplets and Mist apps one way to talk to Exigo: a REST client, a cached SQL pool, a credential interface and typed errors. It handles the connection, authentication and errors. Your app keeps its own business queries.

The package runs on Node 20 or later, including Vercel's Node runtime and Cloud Run. It doesn't run on the Edge runtime.

## Install the SDK

```bash npm theme={null}
npm install @fluid-app/exigo-connection-sdk
```

```bash pnpm theme={null}
pnpm add @fluid-app/exigo-connection-sdk
```

```bash yarn theme={null}
yarn add @fluid-app/exigo-connection-sdk
```

Add these only if your app needs them:

* `mssql` 11 or 12, and `@types/mssql` 9 or later as a dev dependency, to query Exigo's SQL replica.
* `undici` 6 or 7, to call Exigo's REST API through [Fluid's egress proxy](/mist-apps/egress-proxy).

| Import | Provides | Needs `mssql` |
| - | - | - |
| `@fluid-app/exigo-connection-sdk` | `defineExigoCredentials`, the credential types and the errors | No |
| `@fluid-app/exigo-connection-sdk/rest` | `createExigoRestClient` and the errors | No |
| `@fluid-app/exigo-connection-sdk/sql` | `getExigoPool`, `closeExigoPool`, `closeAllExigoPools`, `translateExigoSqlError` and the errors | Yes |

## Define the credentials

The SDK reads no environment variables and stores nothing. You tell it where the credentials come from, once per app. If a field is missing, the SDK throws `ExigoCredentialsError` and names the field.

A droplet that many companies install looks the credentials up per company:

```ts lib/exigo.ts theme={null}
import { defineExigoCredentials } from "@fluid-app/exigo-connection-sdk";

export const resolveExigoCredentials = defineExigoCredentials(
  async (companyId: string) => {
    const saved = await db.exigoCredentials.findUniqueOrThrow({
      where: { companyId },
    });
    return {
      rest: {
        baseUrl: saved.apiBaseUrl,
        username: saved.apiUser,
        password: saved.apiPassword,
        company: saved.companyName,
      },
      sql: {
        host: saved.dbHost,
        database: saved.dbName,
        username: saved.dbUsername,
        password: saved.dbPassword,
        trustServerCertificate: true,
      },
    };
  },
);
```

A Mist app built for one company can return its environment variables from the same function instead.

`trustServerCertificate` is required and has no default. Exigo's replicas present a certificate that doesn't verify against their host name, so they need `true`, which skips certificate verification.

The resolver runs on every call. If the lookup is slow, cache the result in your app.

## Call the REST API

```ts theme={null}
import {
  createExigoRestClient,
  ExigoAuthError,
  ExigoTimeoutError,
} from "@fluid-app/exigo-connection-sdk/rest";

const { rest } = await resolveExigoCredentials(companyId);
const exigo = createExigoRestClient(rest);

try {
  const account = await exigo.get<PointAccount>(
    "/customers/{id}/pointaccounts/{accountId}",
    { params: { id: customerId, accountId: pointAccountId } },
  );
} catch (error) {
  if (error instanceof ExigoAuthError) {
    // The credentials are wrong. Tell the company's admin.
  }
  if (error instanceof ExigoTimeoutError) {
    // Retry, or fail the callback fast.
  }
  throw error;
}
```

`get` and `delete` take `params`, `query`, `headers`, `timeoutMs` and `signal`. `post`, `put` and `patch` also take a `body`, sent as JSON.

The client:

* Adds `/3.0` to the base URL, unless the stored URL already ends in it. A base URL that ends in another version, such as `/2.0`, throws `ExigoCredentialsError`.
* Requires an `https` base URL, because every request carries the password. Plain `http` works only for `localhost`, `127.0.0.1` and `[::1]`, so you can test against a local stub.
* Sends Basic authentication as `username@company`.
* Times out after 15 seconds by default. Set `timeoutMs` on the client or on one call to change it.
* Resolves to the parsed JSON, or to `undefined` when the response body is empty.
* Throws `ExigoAuthError` for a 401 or 403, and `ExigoApiError` for any other status outside the 2xx range or for a 200 response whose body is an HTML page or isn't JSON. The error's message and `bodySnippet` never contain the password.
* Refuses a path that is a full URL, so the credentials only go to the configured host. It also refuses a path that starts with an API version, such as `/3.0/customers`, because it adds the version itself.
* Refuses a path parameter that is empty, `.` or `..`, which would otherwise reach a different endpoint than the path names.

## Query the SQL replica

```ts theme={null}
import { getExigoPool } from "@fluid-app/exigo-connection-sdk/sql";

const { sql: credentials } = await resolveExigoCredentials(companyId);
const pool = await getExigoPool(credentials);

const { recordset } = await pool
  .request()
  .input("email", email)
  .query("SELECT CustomerID FROM Customers WHERE Email = @email");
```

`getExigoPool` returns an `mssql` connection pool. For typed parameters, import the types from `mssql` itself: `request.input("id", sql.Int, id)`.

### How pools are reused

The SDK keeps one pool per host, port, database, user name and TLS settings. Repeated calls reuse it, and calls made while the first connect is still running share that connect. A failed connect isn't kept, so the next call tries again.

When a password changes, the new password gets its own pool and the old one keeps serving the requests already running. The SDK closes the old pool once nothing has used it for about five minutes, and any pool once nothing has used it for about 30 minutes.

The defaults:

| Setting | Default |
| - | - |
| Port | 1433 |
| `encrypt` | `true` |
| Connections per pool (`max`) | 4 |
| Idle connection closed after (`idleTimeoutMs`) | 10 seconds |
| Connect timeout (`connectTimeoutMs`) | 12 seconds |
| Query timeout (`requestTimeoutMs`) | 30 seconds |

Pools are small because every running instance of your app opens its own. To change the pool settings, pass `{ max, idleTimeoutMs, connectTimeoutMs, requestTimeoutMs }` as the second argument. The options apply when the pool opens.

### Errors from SQL

A failed connect throws `ExigoAuthError`, `ExigoTimeoutError` or `ExigoConnectionError`, with a message that says what to check.

The SDK leaves errors from your own queries alone. To map a query's timeout or dropped connection to the same errors, pass the error through `translateExigoSqlError`. Any other query error, such as a wrong table name, comes back unchanged, so it's never reported as a network failure.

### Close pools on shutdown

```ts theme={null}
import { closeAllExigoPools } from "@fluid-app/exigo-connection-sdk/sql";

process.once("SIGTERM", () => void closeAllExigoPools());
```

### Use it in Next.js

Keep `mssql` out of the bundle, and run every route that queries on the Node runtime:

```ts next.config.ts theme={null}
export default { serverExternalPackages: ["mssql"] };
```

```ts app/api/customers/route.ts theme={null}
export const runtime = "nodejs";
```

## Reach Exigo through Fluid's egress proxy

Exigo accepts calls only from IP addresses each tenant allowlists, and a Mist app on Vercel has no fixed outbound address. [Fluid's egress proxy](/mist-apps/egress-proxy) lends your app a fixed one. Both the REST client and the SQL pool take an `egressProxy` option:

```ts theme={null}
import { resolveFluidInstallation } from "@/lib/fluid/installation-context";

// Authorize the caller first: resolveFluidInstallation doesn't.
const { installation, companyId } = await resolveFluidInstallation(request);
const { rest, sql: credentials } = await resolveExigoCredentials(String(companyId));

const egressProxy = process.env.FLUID_EGRESS_PROXY_URL
  ? {
      url: process.env.FLUID_EGRESS_PROXY_URL,
      // The installation's droplet installation token (dit_…).
      token: installation.authenticationToken,
    }
  : undefined;

const exigo = createExigoRestClient(rest, { egressProxy });
const pool = await getExigoPool(credentials, { egressProxy });
```

`resolveFluidInstallation` comes from the Mist app template. It finds the installation from a request header, but doesn't authenticate whoever sent the request. Before a route loads a company's Exigo credentials, check that the caller may act for that company. See [Authentication](/mist-apps/authentication#viewer-identity-with-session-tokens).

When `FLUID_EGRESS_PROXY_URL` isn't set, as in a local run, the code above calls Exigo directly.

* The REST client needs the `undici` package for the proxy. The SQL pool needs nothing extra.
* Clients built with the same proxy URL and token share their connections, so you can create a client per request.
* The proxy URL and token are part of a SQL pool's identity: a different proxy or token gets its own pool.
* Through the proxy, the SQL connection must be encrypted. `encrypt: false` throws `ExigoCredentialsError`. If the replica doesn't offer encryption, the SDK closes the connection before it sends the login and throws `ExigoConnectionError`.
* When the proxy refuses a connection, the SDK throws `ExigoProxyError`, whose `status` is the proxy's answer. See [When the proxy refuses](/mist-apps/egress-proxy#when-the-proxy-refuses).

> [!WARNING]
>
> With `trustServerCertificate: true`, the connection is encrypted, but nothing proves the other end is Exigo. This is true for direct connections too. Closing the gap needs a certificate on Exigo's side that can be verified.

## Errors

Every error extends `ExigoError`.

| Error | When | Extra fields |
| - | - | - |
| `ExigoCredentialsError` | A field is missing or invalid. | `fields` names them. |
| `ExigoAuthError` | The REST API answered 401 or 403, or the SQL login failed. | `status`, for REST. |
| `ExigoTimeoutError` | A REST call, SQL connect or SQL query took too long. | `timeoutMs` |
| `ExigoConnectionError` | DNS, a refused connection, TLS or the network failed. | |
| `ExigoProxyError` | Fluid's egress proxy refused the connection. | `status`, the proxy's answer. |
| `ExigoApiError` | The REST API answered with an error, or with a body that isn't JSON. | `status`, `method`, `path`, `contentType`, `bodySnippet` |

On the auth, timeout and connection errors, `source` is `"rest"` or `"sql"`.

`ExigoProxyError` extends `ExigoConnectionError`, so a check for `instanceof ExigoConnectionError` also catches proxy refusals. Check for `ExigoProxyError` first if your app retries connection failures, since a `403` or `407` from the proxy won't succeed on retry.

The SDK's own messages and fields never contain a password or an `Authorization` header. An error that wraps a driver error keeps the original as `cause`, which the SDK doesn't redact. Don't log or serialize `cause` whole.

## Related pages

* [Egress proxy](/mist-apps/egress-proxy): turn on Fluid's fixed outbound address for your droplet.
* [Connect Exigo to Fluid](/setup/connect-exigo): link a company's Exigo account to Fluid itself.
* [Authentication](/mist-apps/authentication): where a Mist app gets its droplet installation token.


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