# What a module receives

> The ModuleClient a module root is handed by props, the options createChatfuelClient takes, and the helpers that ship in the same barrel.

Page: https://sdk.chatfuel.com/docs/reference/api-client
Markdown: https://sdk.chatfuel.com/docs/reference/api-client.md

A module root is handed a `ModuleClient` through props and never builds one, so nothing in module
code knows about tokens, proxy prefixes or which host it is running inside.

## What a module may call [#what-a-module-may-call]

| Member        | Shape                                                         | What it does                                                                                                                                                                                           |
| ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `query`       | `(doc, variables, opts?) => Promise<TData>`                   | Runs a generated document over HTTP. Throws when the envelope carries `errors[]`; partial data rides on the error.                                                                                     |
| `mutate`      | `(doc, variables, opts?) => Promise<TData>`                   | The same function under a second name — kept separate for intent, identical in behaviour. Variables are stripped of `__typename` either way.                                                           |
| `subscribe`   | `(doc, variables, observer) => () => void`                    | Opens a subscription and returns the unsubscribe, which maps 1:1 onto a `useEffect` cleanup. One lazy shared WebSocket serves every subscription in the app.                                           |
| `onReconnect` | `(cb) => () => void`                                          | Fires after that socket re-establishes following an abnormal close — refetch every query backing a live view, because the server does not replay what you missed. A clean idle close does not fire it. |
| `uploadFile?` | `(botId, file, fileType, pluginId?) => Promise<UploadedFile>` | REST upload, attached by the host when there is an upload path. Absent means no upload UI — GraphQL never accepts file bytes.                                                                          |
| `proxyFetch?` | `(path, init?) => Promise<Response>`                          | An authenticated call to one of the proxy's own routes, not to Chatfuel. `path` is relative to the proxy prefix.                                                                                       |

The last two are optional because the host decides whether to attach them. `proxyFetch` being
present does not promise that a given route exists either — the answer to that is the 404 the route
itself gives. A module offers the half of itself that answers and does without the other half; it
never asks which modules the deployment installed.

`execute` (the envelope without the throw), `iterate` (the async-iterable form of `subscribe`) and
`dispose` are on the full `ChatfuelClient` and deliberately not on `ModuleClient`. The only document
type a module ever names is `TypedDoc`, so the generated shape can change — it has already gone from
an AST to a printed string — without a module noticing.

## Building one [#building-one]

`createChatfuelClient` is the factory, and the host app is the only place that calls it.

<TypeTable
  type="{
  url: {
    description: 'GraphQL endpoint. Behind the dev proxy this is a path, not an origin.',
    type: 'string',
    default: 'https://panel.chatfuel.com/graphql',
  },
  wsUrl: {
    description:
      'Subscription endpoint. A path-only value resolves against window.location at connect time; http/https have their scheme swapped.',
    type: 'string',
    default: 'derived from url',
  },
  token: {
    description:
      'A string or an async getter. Omit it entirely in browser-behind-proxy mode — the proxy injects auth server-side.',
    type: 'string | TokenGetter',
    default: 'none',
  },
  webSocketImpl: {
    description: &#x22;WebSocket class for runtimes without a global one. Node below 22 passes the ws package's.&#x22;,
    type: 'unknown',
    default: 'the global WebSocket',
  },
  fetch: {
    description: 'Test injection.',
    type: 'typeof globalThis.fetch',
    default: 'globalThis.fetch',
  },
  headers: {
    description: 'Extra HTTP headers on every request.',
    type: 'Record<string, string>',
    default: 'none',
  },
  timeoutMs: {
    description: 'Per-request HTTP timeout.',
    type: 'number',
    default: '30000',
  },
  throttle: {
    description:
      'Token bucket plus retries. UI mode leaves it off, because a human paces the requests; a Node script passes BATCH_THROTTLE (5 rps, concurrency 2).',
    type: 'ThrottleOptions | false',
    default: 'false',
  },
  onSessionError: {
    description: &#x22;The proxy gate rejected the caller's session or tenant membership.&#x22;,
    type: '(err: ChatfuelSessionError) => void',
    default: 'none',
  },
}"
/>

This is the whole of the shell's factory — the file every scaffolded app carries, and the one place
that knows where the proxy is mounted:

```ts title="apps/shell/src/client.ts"
export function createAppClient(options: AppClientOptions = {}): ModuleClient {
  const getAuthHeader = async (): Promise<string | undefined> => {
    const value = await options.getAccessToken?.();
    return value ? `Bearer ${value}` : undefined;
  };
  const client = createChatfuelClient({
    url: '/chatfuel/graphql',
    wsUrl: '/chatfuel/graphql',
    token: options.getAccessToken,
    onSessionError: options.onSessionError,
  });
  return {
    query: client.query,
    mutate: client.mutate,
    subscribe: client.subscribe,
    onReconnect: client.onReconnect,
    uploadFile: (botId, file, fileType, pluginId) => uploadFile({ botId, file, fileType, pluginId, getAuthHeader }),
    proxyFetch: async (path, init) => {
      const auth = await getAuthHeader();
      const headers = new Headers(init?.headers);
      if (auth) headers.set('authorization', auth);
      return fetch(`${PROXY_PREFIX}${path.startsWith('/') ? path : `/${path}`}`, { ...init, headers });
    },
  };
}
```

## The token in the browser [#the-token-in-the-browser]

`token` takes a string or a getter, and the getter is resolved per request and per socket connect,
so a rotating credential works without rebuilding the client. A getter that answers `undefined`
sends no `Authorization` header at all.

<Callout type="warn">
  In the browser that value is never the Chatfuel token. It is the user's own session token, sent so
  the proxy's gate can check it; the proxy strips it and attaches the Chatfuel `Authorization`
  server-side, and the WebSocket relay reads it out of the connection-init payload and opens its own
  upstream connection. See [the token boundary](/docs/concepts/token-boundary).
</Callout>

## When the session lapses [#when-the-session-lapses]

`onSessionError` fires when the gate answers `AuthSessionRequired` (the session is missing or
expired) or `AuthTenantForbidden` (the caller is not a member of this deployment's tenant). It fires
once per lapse — the next successful call re-arms it — and it fires from the HTTP and the WebSocket
path alike, so the shell can refresh the session or send the user to sign-in without every module
handling it. Neither code means the Chatfuel token needs rotating; that one is `Unauthorized`, and
the whole family is on [the errors page](/docs/reference/errors).

## Timeouts and retries [#timeouts-and-retries]

The default HTTP timeout is 30 000 ms. `opts.timeoutMs` overrides it for one request, which a
handful of Chatfuel mutations need: `instagramAccountPublishReel` sits on Instagram's transcoder for
up to five minutes, and raising the client-wide default so four operations can finish would mean a
dead upstream is felt five minutes late everywhere. The timeout signal and your own `opts.signal`
are `AbortSignal.any`-ed together, so an abort stays an abort.

Retries only exist when you pass `throttle`, and only for network failures, `429` and `5xx`. A
GraphQL error is never retried. A `429` is always surfaced as an HTTP error rather than an envelope,
which is what lets the throttle see it.

## Helpers in the same barrel [#helpers-in-the-same-barrel]

Everything `~api` exports is in `packages/api-client/src/index.ts`; these are the ones worth knowing
before you write the same code yourself.

| Helper                                    | What it is for                                                                                                                                                                                                                                                                                                                   |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paginate(client, doc, baseVars, select)` | An async generator over a Relay-ish connection, one page of nodes per yield, stopping when `hasNextPage` is false. Sequential by construction — never fetch a page while one is in flight. `baseVars` already carries the page-size variable; the cursor variable is `after` unless you say otherwise.                           |
| `isInvalidCursorError(err)`               | Whether a failure looks like a stale cursor, so you can refetch from the start. Heuristic: the server's exact shape is undocumented.                                                                                                                                                                                             |
| `stripTypename(value)`                    | Recursively drops `__typename`, non-mutating. `query` and `mutate` already do this to variables — reach for it when you are building an input by hand out of a fetched object.                                                                                                                                                   |
| `newClientId()`                           | A fresh UUID for an outgoing message. Must be unique across every client of the account, so generate one per send rather than per component.                                                                                                                                                                                     |
| `backoffDelay(attempt, opts?)`            | `min(base * 2^attempt, cap) * (0.5 + random/2)`. Defaults are the WebSocket reconnect constants: 5 s base, 60 s cap.                                                                                                                                                                                                             |
| `createThrottle(opts)`                    | A token bucket with concurrency and retries, wrapping any promise-returning task. `BATCH_THROTTLE` is the preset for bulk work; Chatfuel's hard limit is 25 requests per second per token.                                                                                                                                       |
| `stableUuid(key)`                         | A v4-shaped UUID derived from a key you choose, same key to same value every time. Segment and filter ids are validated as UUIDs — a readable slug fails the whole query with a generic subgraph error — and a fresh `crypto.randomUUID()` per render would make a list refetch forever. `isUuid` and `UUID_RE` check the shape. |
| `uploadFile(options)`                     | The multipart POST behind `ModuleClient.uploadFile`. A non-2xx throws `ChatfuelHttpError`, whose body snippet carries the platform code (`FileTooBig`, `FileContentTypeNotSupported`) for the UI to match on.                                                                                                                    |
