Skip to content
ChatfuelSDK
ReferenceChatfuel API

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.

AddressNotes
HTTPPOST {base}/graphqlProduction: https://panel.chatfuel.com/graphql. The body is { query, operationName, variables } as JSON.
WebSocketwss://{host}/graphqlSame 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

TransportWhere the token goes
HTTPHeader Authorization: Bearer <token>.
RESTThe same header.
WebSocketThe 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:

EnumValues
BotRoleTypeV2Admin, Editor, Agent, Custom
PermissionAllowedActionNone, View, Edit
PermissionObjectAi, 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.

BehaviourValue
Ping cadence10 s.
Pong wait5 s — a ping unanswered by then is a dead socket, closed with code 4408 so the client reconnects.
Reconnect backoffmin(5s * 2^attempt, 60s) * (0.5 + rand * 0.5), resolved early when the browser fires online.
Close codes not to retry4400 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.

On this page