Skip to content
ChatfuelSDK
Modules

Core

The module with no interface — the skill every other module is built on, and the transport, error and pagination rules the whole app inherits.

What it is

core has no interface. There is no apps/shell/src/modules/core/, no route, no rail item, and nothing in the manifest that would mount one. What it installs is a skill — the one every other chatfuel-* skill declares as required — and what that skill holds is the set of facts nobody should have to rediscover per module: how you reach the API, what a token is, what an error looks like, how a connection pages, the whole schema as a file, and a script that checks an operation against it before you run it.

That is also why it is the one module you never choose. It is installed with everything.

Installed as

Wizard idcore
Install itYou don't — --modules never needs it
Selected byAlways. Every run installs it.
RequiresNothing
Recommends
Skillchatfuel-core

Neither hidden nor railHidden, because neither flag applies: those describe how a module appears in the shell, and this one has no shell presence to describe. core is implicit for every module and is never listed in a requires array — the wizard's dependency closure calls visit('core') before anything the caller asked for, so a manifest that named it would be saying something the registry already guarantees.

Routes and views

There is no route. The manifest carries no app block at all, which is what a module uses to declare an entry component, an embed root and its environment. Without one there is nothing to mount, nothing to navigate to, and nothing to delete when a scaffold drops a module.

The model

Three inputs, and everything else follows from them: a base URL (https://panel.chatfuel.com in production), an API token, and a botID. There is one endpoint per environment, not per account:

HTTPPOST {base}/graphql
WebSocketwss://{host}/graphql, subprotocol graphql-transport-ws
REST{base}/api/… — file uploads

Authentication is Authorization: Bearer <token> over HTTP and REST. A browser cannot set headers on a WebSocket, so there the token rides in the connection_init payload as authToken, with the Bearer prefix included. The credential is a dashboard user token that the account owner generates in the Chatfuel panel — treat it as opaque, with no assumed shape or lifetime. A bot's own apiToken is a different thing entirely and does not work for GraphQL at all.

A proxy is not optional for anything with a browser in it. Production CORS allows exactly one origin, https://panel.chatfuel.com, so a browser app on any other domain fails the preflight for POST /graphql — and the panel's frame-ancestors policy rules out an iframe too. Server-to-server integrations involve no CORS and need no proxy. The asymmetry worth knowing: a raw WebSocket is not subject to preflight, so subscriptions could connect from a foreign origin — which would require shipping a full-account credential to the browser, so proxy both. That is the constraint the whole app is shaped around; the deployed form of it is on the token boundary.

Four things about the graph are worth knowing before you write a query. The hierarchy is UserAccount → Workspace → Bot → ContactScope → Contact → Conversation, and almost everything is scoped by botID: there are only about fourteen root Query fields, so you read data by traversing bot(id:) and currentUser rather than by per-entity root queries. Message types, send mutations and several fields are per-platform, so branch on Conversation.platform or __typename. Conversation.id is the contact id — a server-side alias, and every conversationID argument takes the contact's id. And real time is GraphQL subscriptions over one lazy WebSocket, with lists arriving as Add/Update/Remove edge events the client merges and re-sorts itself.

A 401-equivalent can arrive inside an HTTP 200. The gateway may answer 200 with an errors[] array carrying an Unauthorized code from a subgraph, so a client that only checks the HTTP status treats a dead token as an empty result. Scan errors[].extensions.code — and one level deeper: a relayed subgraph failure reads "Failed to fetch from Subgraph 'x'" at the top and carries the real code and traceId in extensions.errors[]. data and errors can also coexist, so partial data is a case each view has to decide about rather than a state that cannot happen.

Pagination is Relay-shaped and then not: every connection has its own cursor scalar and its own PageInfo type — there is no shared one — cursors are opaque, and first is required on some fields and optional on others. Conversation.messages is the one to read the signature for: with no cursor or with after it runs newest-first and after walks backwards into history, while before runs ascending.

Configuration

The manifest declares no app.env at all, so this module contributes no environment variable of its own. What the app actually reads comes from the modules you installed beside it — in practice CHATFUEL_TOKEN, VITE_CHATFUEL_WORKSPACE_ID and CHATFUEL_API_BASE, which every surface module declares. All of them, and which side reads each, are on environment variables.

Permissions

The manifest declares no permissions. core asks for no Chatfuel object and no action, because it performs no operation — the modules built on it do.

The permission model it documents is per-bot and per-role (Admin | Editor | Agent | Custom), with (object, action) pairs: Inbox gates live chat, People gates contacts, and ContactsAssignedToOthers / ContactsUnassigned restrict visibility — a restricted agent receives UnavailableContact stubs rather than an error. Check what a token can actually do with the MyBotRole operation before building a screen that assumes edit rights.

Limits

Twenty-five requests a second per token, authenticated. Query depth and field-count guards exist too; the operations in the skill's examples/ are far below them.

Introspection is disabled in production. The bundled references/schema.graphql is not a convenience — it is the only schema source there is. It is a copy of the dashboard's schema, with the source commit named in its header, trimmed by script of the billing, Albato, A/B-experiment and debug surfaces. Validate every operation against it before running it live; that is what scripts/validate-operations.mjs is for, and with no arguments it validates this skill's examples and every sibling chatfuel-* skill's.

Three fields will bite whoever writes the first query. Always select __typename on Contact and Conversation — a known backend bug causes errors otherwise, and selecting it on every interface and union is cheap insurance. clientId on a message must be a fresh UUID, unique across all clients of the account, because the dashboard merges by it and a collision corrupts both your list and theirs; Message.id is nullable and clientId is the reliable key. And never query GetDefinedErrorCodes: it exists only to union the error enum across subgraphs and always errors when called.

Pagination has three specific liars. Bot.whatsAppTemplates accepts first, after and before and ignores all of them, always returning the full list. Seven fields have no pagination at all — Bot.goodsCatalog, Bot.specialists, Bot.contactScopes, Bot.members, Bot.invites, Workspace.bots and CurrentUserAccount.workspaces. And an after cursor that has fallen out of the result window answers an invalid-cursor error; the recovery is refetching from the start.

The socket guarantees "from now on" and nothing more. One lazy connection per token, a ping about every ten seconds with a dead socket declared at fifteen, exponential backoff with jitter, and six close codes not to retry on (4400, 4401, 4403, 4406, 4409, 4429). On every reconnect, refetch the queries behind every live view: the server does not replay what was missed while you were away.

Strip __typename from variables. Round-tripping a fetched object into a mutation input without recursively removing it is rejected — the server refuses unknown input fields.

On this page