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 content/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 — on your server's own socket to the API, never in a browser. 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 credential entirely: it is scoped to one bot, it works only on the separate REST APIs, and it reaches Chatfuel with no proxy in front of it, which is why the proxy's gate refuses a selection that would read it out of the schema.

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.

There is one token, it holds the whole account, and it does not tell the deployment's customers apart. Nothing upstream will draw a tenant boundary for you, which is why the proxy is the thing that decides which bot a caller may name rather than asking the caller. That is the constraint the whole app is shaped around; the deployed form of it is on the token boundary, and what a caller past the gate may still do is on ceilings and fences.

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 twelve 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 API may answer 200 with an errors[] array carrying an Unauthorized code, 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 failure carries a generic message at the top and 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

The API rate-limits per token, and the ceiling is not published in the schema. Batch reads sensibly and back off on a 429-shaped error. Query depth and field-count guards exist too; the operations in the skill's examples/ are well inside them.

Introspection is not available. The bundled references/schema.graphql is not a convenience — it is the only schema source there is. It is a generated snapshot carrying the surfaces a scaffolded app works with, names and types only: no descriptions and no deprecated members. Two things follow. A name you cannot find is not necessarily a name you got wrong — it may be one the bundle deliberately does not carry, and a name that is not in it is a name to treat as unavailable here. And which error codes a given mutation can return is not in the file at all, so take them from the module guide covering that surface and switch on the raw extensions.code string with a fallback branch rather than on an exhaustive enum.

Validate every operation against that snapshot 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. The SDL and its possible-types.json are not checked into the skill directory: one copy lives in content/schema, and the wizard writes both into the installed core skill's references/ so the file every skill points at is the file the codegen reads.

An app the wizard wrote refuses a document it does not ship. Its proxy checks every request against the operation documents the app exports and answers 403 OperationNotInRegistry for anything else — including a field added to an existing operation, since that is a different document. So an operation of your own has to be exported the way the others are, not merely written somewhere the client can reach.

Three fields will bite whoever writes the first query. Always select __typename on Contact and Conversation — fields come back missing 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 everything writing to that account merges by it and a collision corrupts both lists; 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 returns the full list regardless. 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