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 the rate limit.
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.
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.
One account shape is not served by this API at all. On a legacy account an auth attempt returns
AuthResultRedirectToOldDashboard instead of AuthResultRegular; that account lives on the old
Chatfuel product on a different host. Passing forceAuthFromCfProductsSwitcher: true to the auth
mutations keeps eligible accounts on the new 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.
The rate limit
25 requests per second per token, for an authenticated caller. The SDK's client takes an optional
token bucket — a requests-per-second rate and a concurrency cap, written against 25 rps as the hard
limit, 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. Network failures, 429 and 5xx are
retried; GraphQL and auth errors never are.
Query size guards also exist, as depth and field caps. Nothing in the shipped operations comes close to them.
Where several people share one token through a proxy, a per-user limit below 25 rps 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 four rules that catch people.