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

Page: https://sdk.chatfuel.com/docs/modules/inbox
Markdown: https://sdk.chatfuel.com/docs/modules/inbox.md

## What it is [#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 [#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 [#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 [#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.

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

## Configuration [#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](/docs/deploy/environments).

## Permissions [#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 [#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   |

<Cards>
  <Card title="Inbox" href="/modules/inbox" description="What this module does, from the product side: the five channels, take-over, close-to-flow." />

  <Card title="Contacts" href="/docs/modules/contacts" description="Where the contact card comes from, and where a contact id is found in the first place." />

  <Card title="Your agent's notes" description="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." />
</Cards>
