---
title: "Making Public API Requests"
description: "Send GraphQL POST requests to panel.chatfuel.com/graphql, paginate with cursors, subscribe over WebSocket, upload files and stay within the rate limits."
canonical_url: https://chatfuel.com/docs/public-api/requests
markdown_url: https://chatfuel.com/docs/public-api/requests.md
last_updated: 2026-10-06
lang: en
site: https://chatfuel.com/docs
llms_txt: https://chatfuel.com/llms.txt
---

# Making Public API Requests

Send GraphQL POST requests to panel.chatfuel.com/graphql, paginate with cursors, subscribe over WebSocket, upload files and stay within the rate limits.

The Chatfuel Public API is a single GraphQL endpoint, `https://panel.chatfuel.com/graphql`. Queries and mutations are HTTP `POST` requests with a JSON body; subscriptions use a WebSocket on the same address; files are uploaded to a separate HTTP endpoint and then referenced by ID. Every request carries a token in the `Authorization: Bearer <token>` header, as described in [authentication](https://chatfuel.com/docs/public-api/authentication).

## How to send a query or mutation [#how-to-send-a-query-or-mutation]

The request body is a JSON object with `query` and, optionally, `variables` and `operationName`. Always pass values through `variables` instead of building the query string by hand.

Node.js 18 or later:

```js
const ENDPOINT = 'https://panel.chatfuel.com/graphql';

async function gql(query, variables) {
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.CHATFUEL_VIRTUAL_USER_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  if (body.errors) throw new Error(JSON.stringify(body.errors));
  return body.data;
}

const data = await gql(
  `query Bot($id: BotID!) { bot(id: $id) { id title timezone } }`,
  { id: process.env.BOT_ID },
);
```

Python with `requests`:

```python
import os
import requests

ENDPOINT = "https://panel.chatfuel.com/graphql"

def gql(query, variables=None):
    res = requests.post(
        ENDPOINT,
        json={"query": query, "variables": variables or {}},
        headers={"Authorization": f"Bearer {os.environ['CHATFUEL_VIRTUAL_USER_TOKEN']}"},
        timeout=30,
    )
    res.raise_for_status()
    body = res.json()
    if body.get("errors"):
        raise RuntimeError(body["errors"])
    return body["data"]

data = gql("query Bot($id: BotID!) { bot(id: $id) { id title } }", {"id": os.environ["BOT_ID"]})
```

A GraphQL response can have HTTP status 200 and still contain `errors`. Always check the `errors` array, as described in [errors](https://chatfuel.com/docs/public-api/errors).

## How to paginate through lists [#how-to-paginate-through-lists]

Long lists are cursor-paginated connections: you pass `first` (page size) and `after` (the cursor of the last item you have), and read `edges { node cursor }` plus `pageInfo { hasNextPage endCursor }`. Cursors are opaque strings; store them as they are.

```js
async function allContacts(botID) {
  const query = `
    query Contacts($botID: BotID!, $after: ContactSearchCursor) {
      bot(id: $botID) {
        contactsConnection(
          platforms: [whatsapp, instagram, facebook, tiktok, widget]
          first: 500
          after: $after
        ) {
          edges { node { id name } }
          pageInfo { hasNextPage endCursor }
        }
      }
    }`;
  const contacts = [];
  let after = null;
  do {
    const { bot } = await gql(query, { botID, after });
    const page = bot.contactsConnection;
    contacts.push(...page.edges.map((e) => e.node));
    after = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
  } while (after);
  return contacts;
}
```

The largest page is 500 items for contacts, conversations and keyword rules. Smaller pages of 50 to 100 items keep responses fast when you select many fields.

## How to subscribe to events [#how-to-subscribe-to-events]

Subscriptions push events such as new messages, contact updates and booking changes. They run over WebSocket at `wss://panel.chatfuel.com/graphql` with the `graphql-transport-ws` protocol, implemented by the `graphql-ws` library. Pass the token in the connection parameters as `authToken`, including the `Bearer ` prefix:

```js
import { createClient } from 'graphql-ws';
import WebSocket from 'ws';

const client = createClient({
  url: 'wss://panel.chatfuel.com/graphql',
  webSocketImpl: WebSocket,
  connectionParams: { authToken: `Bearer ${process.env.CHATFUEL_VIRTUAL_USER_TOKEN}` },
  keepAlive: 10_000,
});

client.subscribe(
  {
    query: `subscription NewMessages($botID: BotID!, $conversationID: ConversationID!) {
      messageAdded(botID: $botID, conversationID: $conversationID) { id sentTime }
    }`,
    variables: { botID: process.env.BOT_ID, conversationID: process.env.CONTACT_ID },
  },
  { next: (event) => console.log(event.data), error: console.error, complete: () => {} },
);
```

Permissions are checked when you subscribe and again for every event, so a virtual user only receives events it is allowed to see. Each reference page lists its subscriptions. Reconnect with backoff when the socket closes.

## How to upload files [#how-to-upload-files]

Images, videos, audio and documents are uploaded over plain HTTP and then passed to mutations by their file ID. Send the file as `multipart/form-data` to the upload endpoint with the bot ID and the file type (`Image`, `Video`, `Audio` or `Document`):

```bash
curl -s "https://panel.chatfuel.com/api/filestorage/upload/bot?botID=$BOT_ID&fileType=Image&extension=jpg" \
  -H "Authorization: Bearer $CHATFUEL_VIRTUAL_USER_TOKEN" \
  -F "file=@product.jpg"
```

The response is JSON describing the stored file; its `id` is the `FileID` you pass to mutations such as `goodsProductCreate`, `whatsAppImageSetImageFile` or `csvContactImportCreate`. Uploading to a bot needs the Editor role. Use the file ID within two hours: uploads that are never used are deleted.

To attach a file to a chat message, upload it to the chat endpoint, which also takes the contact ID and works for Agents:

```bash
curl -s "https://panel.chatfuel.com/api/filestorage/upload/livechat?botID=$BOT_ID&contactID=$CONTACT_ID&fileType=Document&extension=pdf" \
  -H "Authorization: Bearer $CHATFUEL_VIRTUAL_USER_TOKEN" \
  -F "file=@invoice.pdf"
```

Template header media and the website widget avatar have their own upload endpoints. [Files and tasks](https://chatfuel.com/docs/public-api/files-and-tasks) lists all of them and describes the `File` type.

## Rate limits [#rate-limits]

The Public API allows **25 operations per second per account**. Each virtual user has its own allowance. A personal API token shares its allowance with your own dashboard session, which is one more reason to run integrations as virtual users. Requests without a token are limited to 40 per second per IP address.

One GraphQL operation counts once, however many fields it selects. When you go over the limit, the request is rejected with status `429 Too Many Requests`. Wait about a second and retry with exponential backoff; batch reads into one query where you can.

## Query size limits [#query-size-limits]

To protect the service, each operation is limited to a nesting depth of 300, 10,000 selected fields in total and a complexity of 8,000 per Chatfuel service, where complexity grows with every selected field. A query over a limit is rejected before it runs. Select only the fields you use and split very large reads into several queries.

## Common issues [#common-issues]

### "429 Too Many Requests" [#429-too-many-requests]

More than 25 operations per second ran under the same account. Add backoff and retries, combine reads into fewer queries, or spread the load over several virtual users.

### The WebSocket connects but every subscription fails with "Unauthorized" [#the-websocket-connects-but-every-subscription-fails-with-unauthorized]

The `authToken` connection parameter must include the scheme: `Bearer <token>`. A bare token is ignored.

### The upload endpoint returns 401 [#the-upload-endpoint-returns-401]

The token is missing, or the virtual user's role doesn't allow the upload: bot uploads need Editor, chat uploads need Agent or Editor. Check that `botID` belongs to the virtual user's bot.
