# 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.

Page: https://sdk.chatfuel.com/docs/reference/chatfuel-api/transport-auth
Markdown: https://sdk.chatfuel.com/docs/reference/chatfuel-api/transport-auth.md

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](/docs/reference/chatfuel-api/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 [#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`:

```ts
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](/docs/concepts/token-boundary).

## What the token is [#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 [#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:

```graphql
query MyBotRole($botID: BotID!) {
  currentUser {
    id
    botRole(botID: $botID) {
      ...RoleInfo
    }
  }
}
```

## How the WebSocket client behaves [#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.

<Callout type="warn">
  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.
</Callout>

## The rate limit [#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.
