Transport and auth
The three endpoints, how the Chatfuel token travels on each of them, what the token is and what it may do, how the WebSocket client behaves, and how rate limiting is handled.
There is one endpoint per environment rather than one per account, and the botID inside the
operation selects the project.
| Address | Notes | |
|---|---|---|
| HTTP | POST {base}/graphql | Production: https://panel.chatfuel.com/graphql. The body is { query, operationName, variables } as JSON. |
| WebSocket | wss://{host}/graphql | Same path, TLS. Subprotocol graphql-transport-ws — the graphql-ws library, not the legacy subscriptions-transport-ws. |
| REST | {base}/api/... | File uploads. See files and tasks. |
Appending ?op=<OperationName> to the HTTP URL is optional and useful: the server ignores it and
the operation still travels in the POST body, but proxy and upstream logs become readable. The
SDK's client adds it for every named operation.
How the token travels
| Transport | Where the token goes |
|---|---|
| HTTP | Header Authorization: Bearer <token>. |
| REST | The same header. |
| WebSocket | The connection_init payload, under the key authToken, with the Bearer prefix included in the value. |
A browser cannot set a header on a WebSocket handshake, which is why the socket carries the
credential in a message instead. With the graphql-ws client that is connectionParams:
connectionParams: async () => {
const value = options.getToken ? await options.getToken() : undefined;
return value ? { authToken: `Bearer ${value}` } : {};
},A raw WebSocket is not subject to a CORS preflight, so a subscription can technically connect from a foreign origin — but only by shipping a full-account credential to the browser. Both transports go through a proxy for that reason; see the token boundary.
What the token is
A Chatfuel token is a dashboard user token. The account owner generates it at
https://panel.chatfuel.com/integration/auth/token, and rotation means generating a new one
there. Assume nothing about its shape or its lifetime: it is an opaque secret that can be
regenerated at any time, and it is a full-account credential, so it belongs on a server.
bot.apiToken is a different credential. It authenticates the separate REST APIs — broadcasting,
contact import — and does not work for GraphQL. It is scoped to one bot, it outlives the session,
and it reaches Chatfuel with nothing in front of it, so reading it once out of the schema is a
permanent way around whatever your own backend does.
When a token stops working, every request comes back with an Unauthorized code. That is a
rotation signal, not a transient failure: surface it and stop, rather than retrying.
An auth attempt can also come back as AuthResultRedirectToOldDashboard rather than
AuthResultRegular. That is not a session and nothing downstream works with it — handle both
branches of the union. Passing forceAuthFromCfProductsSwitcher: true to the auth mutations keeps
eligible accounts on this product.
What the token may do
Permissions are per bot. A role is a type plus a list of (object, action) pairs:
| Enum | Values |
|---|---|
BotRoleTypeV2 | Admin, Editor, Agent, Custom |
PermissionAllowedAction | None, View, Edit |
PermissionObject | Ai, Configure, Roles, Pro, Inbox, Broadcasting, People, Analyze, Home, Bot, Workspaces, Flows, ContactsAssignedToOthers, ContactsUnassigned |
Inbox gates live chat, People gates contacts, and ContactsAssignedToOthers /
ContactsUnassigned are visibility restrictions — a restricted caller still receives a Contact,
but it is an UnavailableContact stub with every field empty. The two actions are independent:
Edit does not imply View, so a gate that means "may open this page" lists both pairs.
MyBotRole is the operation that answers all of it before you build UI that assumes edit rights:
query MyBotRole($botID: BotID!) {
currentUser {
id
botRole(botID: $botID) {
...RoleInfo
}
}
}How the WebSocket client behaves
One lazy connection per token: it opens on the first subscription and every subscription after that shares it.
| Behaviour | Value |
|---|---|
| Ping cadence | 10 s. |
| Pong wait | 5 s — a ping unanswered by then is a dead socket, closed with code 4408 so the client reconnects. |
| Reconnect backoff | min(5s * 2^attempt, 60s) * (0.5 + rand * 0.5), resolved early when the browser fires online. |
| Close codes not to retry | 4400 BadRequest, 4401 Unauthorized, 4403 Forbidden, 4406 SubprotocolNotAcceptable, 4409 SubscriberAlreadyExists, 4429 TooManyInitialisationRequests. |
graphql-ws already hard-fails on all six except 4403, so a client on that library needs to add
only that one to its retry predicate. A clean idle close (1000) from the lazy connection is not a
reconnect and must not trigger refetches.
The socket guarantees "from now on" and nothing else — the server does not replay events missed while you were disconnected. On every reconnect, refetch the queries behind every live view: the chat list, the open thread, the counters. Skip it and the view keeps rendering, quietly stale.
Rate limiting
The API rate-limits per token, so batch reads and back off on 429. The SDK's client takes an
optional token bucket — a requests-per-second rate and a concurrency cap, with a bulk preset of
5 rps at concurrency 2 — and surfaces every 429 as an HTTP error rather than a GraphQL one so the
retry path can see it. Without that option there is no throttling at all, which is the right
default for a UI a human is pacing.
What gets retried depends on whether a repeat is safe:
| Failure | Retried |
|---|---|
429 | Always. The server turned the request away before running it, so a repeat cannot double anything. |
A network failure or a 5xx | Only for a task the caller marked idempotent. The request may have reached Chatfuel, run, and lost its answer on the way back. The client marks queries idempotent and mutations not. |
| A GraphQL error, an auth error | Never. |
Query size guards also exist. Nothing in the shipped operations comes close to them.
Where several people share one token through a proxy, a per-user limit under Chatfuel's own ceiling is what stops one of them exhausting the budget for everyone.
The Chatfuel API
The mental model behind the GraphQL API a generated app talks to — the hierarchy every operation traverses, the two automation surfaces that are not the same thing, and why the schema ships as a file.
Errors
The Chatfuel API's error envelope — machine-readable codes in extensions, the DefinedErrorCode family, and the rules that catch people.