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.
Last updated on
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.
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:
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:
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.
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.
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
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:
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
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):
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 "[email protected]"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:
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 "[email protected]"Template header media and the website widget avatar have their own upload endpoints. Files and tasks lists all of them and describes the File type.
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
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
"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 authToken connection parameter must include the scheme: Bearer <token>. A bare token is ignored.
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.