Inbox
The operator inbox over five channels — its one route, the identity rule the whole module rests on, and the platform limits that arrive after a send.
What it is
There is no plain close in this API. An operator finishes a conversation by handing the contact back to a bot flow, and that hand-over is the close — which is why the module is arranged around two writes, take-over and hand-over, rather than around a status field.
Between them sits the operator's side of the bot: a chat list, a message thread and a composer,
live over one WebSocket, at /livechat. WhatsApp, Instagram, Facebook, TikTok and the web
widget arrive in the same list, and a conversation reads Automated for exactly as long as the
bot has it, Open once somebody takes it.
Installed as
| Wizard id | livechat |
| Install it | npx @chatfuel/wizard --modules livechat |
| Selected by | Default — --yes installs it |
| Requires | Nothing, beyond the implicit core |
| Recommends | contacts |
Routes and views
One path, and no view segment: the list, the thread and the contact card are three panes of one screen, not three views.
| Path | What it shows |
|---|---|
/livechat | The chat list, the open thread, and the contact panel beside it. |
| Parameter | What it does |
|---|---|
c | The open conversation. Written back as you move, so the address stays a shareable link — and it opens a thread even when the conversation is not in the list. |
contact | Start, or find, a conversation with this contact and open it. Consumed once: it is an instruction, not a state, and it is deleted on the next write. |
Both are read once at mount. The keyboard is j and k down and up the list, e to hand over,
a to assign, / to search and ⌘K for the palette — all of them stand down while you are
typing, so "let me take a look" does not close the conversation on the e.
The model
Conversation.id is the contact id. There is no separate conversation identity anywhere
in this API: every conversationID argument takes the contact's id, and passing one where the
other is asked for is correct rather than a bug to fix.
That is what makes /livechat?contact=<id> work at all. The inbox's own operations have no
contact search — the chat list searches contacts that already have a conversation, which is
exactly the set that link is not for — so another module hands a person over by id, and
CreateConversation means "ensure a conversation exists", so an id that already has one opens
that conversation rather than making a second. The contacts module builds that link directly.
The other identity to know is the message's. Message.id is nullable on the wire and clientId
is not, so clientId is the key you merge on — a fresh UUID per message, unique across every
client writing to the account, because the Chatfuel dashboard merges on it too.
A send mutation answers "accepted", not "delivered". The WhatsApp 24-hour window, Instagram's
reply window, TikTok's ten messages per 48 hours, a template Meta paused overnight — every one
of them arrives later, on the message, through messageUpdated. A client that reports only
mutation errors therefore renders a perfectly normal-looking bubble for a message the contact
will never receive.
Configuration
| Variable | Notes |
|---|---|
CHATFUEL_TOKEN | Secret. Server-side only. |
VITE_CHATFUEL_WORKSPACE_ID | The workspace you picked during the wizard run. |
CHATFUEL_API_BASE | Defaults to https://panel.chatfuel.com. |
Every variable the app reads, and which side reads it, is on environment variables.
Permissions
| Object | Action | Required for |
|---|---|---|
Inbox | View | Reading the inbox. |
Inbox | Edit | Sending, mark-as-read and take-over. |
People | View | The contact panel beside the thread. |
People: View is not in this module's manifest — it is the contacts permission the panel
borrows, and the skill names it beside the other two.
Limits
Only WhatsApp's window is gated. The composer greys 24 hours after the contact last wrote, and the template button stays lit as the way back in. Instagram's reply window and TikTok's ten-per-48-hours are deliberately not gated: TikTok's is a count over a period, and the count depends on history that may not be loaded. A gate that is right most of the time silently forbids messages an operator is allowed to send, so those two come back as errors on the bubble instead.
A message is a file or text, never both. The attachment mutations take one FileID and no
text, so a photo with a caption is two messages, each with its own clientId. The file goes
first.
The channels take different files, and the differences do not rhyme. The web widget and TikTok take images and nothing else; WhatsApp takes documents but no video; Instagram and Facebook take video but no documents. The module reads that off the generated enums rather than transcribing it, and refuses a file the channel cannot carry before a byte is uploaded.
Some messages carry nothing to render. FacebookInFileMessage has no fileName field at
all, a WhatsApp template on the wire has no template name, and the placeholder and unknown
message types have no payload fields and no status field — the only honest rendering is a
labelled chip.
A widget preview conversation is read-only. You can read it, mark it read and open its
contact card, but conversationStart and a text send both answer InternalServerError, and it
is never listed by the chat list. To exercise sending, take-over and close, write to the bot
from a real channel.
The list is live but positionless. Updates arrive as add / update / remove batches with no positions, so you upsert and re-sort by last message; and the subscription's filters must match the query's exactly or you will merge events from a different result set. Thread subscriptions are per open conversation — subscribe on open, drop on close — and events during a WebSocket gap are lost, so a reconnect refetches.
Nothing here is shared with your team. There is no canned-response type and no saved-view
type anywhere in the schema. Both live in setUserStorageItem, scoped to the signed-in user, so
two operators on the same bot do not see each other's.
One query shape is a 500 with no message. Selecting post { id } on
FacebookInPostCommentMessage in a document that also selects TikTok's post makes the router
reject its own merged operation — conflicting scalar types under the same field name. Over HTTP
that is a bare internal server error for the whole message query, and the planner's actual
complaint is visible only when the same selection is sent as a subscription.
| What | Number |
|---|---|
| Conversations in a list page | 50 |
| Messages in a thread page | 50 |
| WhatsApp templates per page | 50 |
| Attribute catalog page | 100 |
| Files staged in the composer at once | 10 |
| Saved replies | 50, with a 60-character title and a 2,000-character body |
| Saved views | 50, with a 60-character name and a 200-character query |
Inbox
What this module does, from the product side: the five channels, take-over, close-to-flow.
Contacts
Where the contact card comes from, and where a contact id is found in the first place.
Your agent's notes
The wizard installs the module's skill as .claude/skills/chatfuel-livechat/ for Claude Code, or .agents/skills/chatfuel-livechat/ for Codex CLI. Its references/guide.md carries the chat list, the message-shape table, sending, the lifecycle and the cache-merge rules.