Skip to content
ChatfuelSDK
ReferenceAPI client

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.

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

MemberShapeWhat 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) => () => voidOpens 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) => () => voidFires 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

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

Prop

Type

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:

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

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.

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.

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.

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

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

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

On this page