Skip to content
ChatfuelSDK
Modules

Channels

Every channel the bot is connected to, on one route — the hand-off that connects one, the two keys the return leg writes, and the refusals that cannot be told apart.

What it is

Nothing in this API carries somebody through a platform's consent screens. The OAuth round trip, the callback and WhatsApp's embedded sign-up all belong to pages Chatfuel serves, so connecting is a hand-off rather than a form: you press Connect, the app mints a one-shot platform link with both redirects pointing back at this page, and the browser leaves for Chatfuel in the same tab. You finish there and come back here with the channel connected.

Everything else on the page is what is already connected, read out of bot.contactScopes — the WhatsApp number, the Instagram account, the TikTok account, the Facebook pages and the web widget, each with what it is connected as and a Disconnect where the server allows one. The three that take a link also carry Refresh access, which re-grants the permissions on the asset already connected and touches nothing else.

Installed as

Wizard idchannels
Install itnpx @chatfuel/wizard --modules channels
Selected byDefault — --yes installs it
RequiresNothing, beyond the implicit core
RecommendsNothing
Skillchatfuel-channels

The rail says Channels and the shell's nav table puts the item under Settings. core is implicit for every module, and this one leans on it: it ships no operations of its own, so the channels read, the disconnect, the two link creates and the role check all come from core's generated documents.

Routes and views

/channels is the whole address. There are no views, and the only query parameters are the two the return leg of a hand-off writes:

ParameterWhat it does
resultconnected or failed — which of the two redirects Chatfuel followed.
channelThe platform the hand-off was for: whatsapp, instagram or tiktok.

Anything else in either key is no arrival at all. Both are read once, said once, and taken back out of the address with a replaceState that leaves no entry in the nav stack — so a reload after a hand-off is a plain load of the page.

What the page draws is a card per platform, in a fixed order: WhatsApp, Instagram and TikTok, then Facebook and the web widget as lists. WhatsApp, Instagram and TikTok hold one asset each, because the server refuses a second of the same platform. Facebook pages come in any number and are sorted by name. The web widget is one per bot and prints no row at all: its name is the empty string on every bot, and a row holding a blank is a card body with nothing in it — the chip beside the title already says whether anything is connected.

An empty platform card carries Connect. A connected one carries Refresh access and Disconnect, and Disconnect asks first: The bot stops receiving messages on this channel. A Connect or a Refresh access the API refuses prints the reason under the card; a Disconnect that fails keeps its confirm dialog open with the reason in it, where the reader is already looking. The header carries a Refresh.

The model

The link is minted on the press and spent by the redirect that follows. It is never shown, never copied and never kept: it is a credential whose job starts immediately, and the person it could be handed to is the one already sitting in front of the screen. Refresh access works the same way, with the other kind of link. The address Chatfuel answers with is checked against the same scheme allowlist as any other URL that came off the wire before the app follows it — and when it is refused, the card says so rather than leaving quietly.

The redirects only exist over https. The API refuses a redirect that is not https:// with a host, so a deployment served over plain http — every npm run dev — mints the link with no redirects at all rather than one the API would refuse. The hand-off still works there; what is missing is the leg home, and the way back is the browser's own Back button. This is the first thing that looks like a bug and is not.

Arrival is not proof. Anybody can type the return address, so result decides what the page says, never what is true: a success is a toast, a failure is a dismissable alert saying the hand-off came back without finishing, and what is on screen either way comes from the read that runs on mount.

Two reads, and only one of them needs a permission.

DocumentReadsNeedsWho sees it
BotChannelsbot.contactScopesAccess to the botEvery role
MyBotRoleThe caller's permissionsEvery role

The active-link maps — Bot.activePlatformConnectionLinks and activePlatformAccessRefreshLinks — are not read here at all. A link this app spends in the same gesture is never a thing to keep in sync. They exist for the other shape a link can be spent in: an agency minting one to send to a client who has no account in the product, which is what the url, the maps and the revoke mutations are for. Should you build that shape, keep the maps out of the channels document: both fields are non-null and both need Configure: Edit, so one document asking for scopes and links together answers nothing at all to a role that may read but not manage.

Creating a link replaces the active one for that platform and kind, silently — no error, no field that says so, and the API keeps no used, expired, revoked or replaced state to look it up in. Here that costs nothing, because the link is spent in the gesture that made it. A link also expires 24 hours after it is created, and one successful connection consumes it.

Nothing subscribes to any of this. A channel is connected on Chatfuel's own page, so the page re-reads on reconnect, on a tab coming back after a minute away, and on the header's Refresh. Disconnect needs no refetch — it answers Bot with the fresh scopes, and they replace what is held. Nothing is optimistic, and an answer from a load that a refresh has already overtaken is dropped rather than painted.

Configuration

VariableNotes
CHATFUEL_TOKENSecret. Server-side only.
VITE_CHATFUEL_WORKSPACE_IDThe workspace you picked during the wizard run.
CHATFUEL_API_BASEDefaults to https://panel.chatfuel.com.

Those three and nothing else. Every variable the app reads, and which side reads it, is on environment variables.

Permissions

ObjectActionRequired for
BotViewReading which channels are connected to the bot.
ConfigureEditConnect, Refresh access and Disconnect.

A role without Configure: Edit sees the connection state and no control to change it: the three buttons are absent rather than disabled. The gate is closed until MyBotRole answers, and an error closes it.

That is a decision about what to offer, not an authorization boundary. Through the proxy the API enforces the token owner's role, so a host that wants a harder line draws it in the proxy.

Limits

Three platforms take a link. PlatformOperationLinkPlatform has whatsapp, instagram and tiktok. There is no facebook member: a Facebook page is connected by a signed-in operator through the OAuth route, never by a link, and the web widget needs no connection at all. The widget cannot be disconnected either — the server refuses with CannotDisconnectWidgetScope — so that row carries no control.

contactScopes has no pagination and no order. The whole list arrives at once, in no order worth relying on, so the page takes the first scope of each single-asset platform, sorts the Facebook pages by name, and skips a __typename it does not know rather than throwing on it.

A TikTok account may have no handle. Both username and name are nullable there, where Instagram's username is not, so the card falls back to the account id.

Two refusals cannot be read from their code. CannotDisconnectWidgetScope arrives nested, at errors[0].extensions.errors[0].extensions.code, so a check that reads only the top level misses it. And a contact scope the bot does not have answers InternalServerError — the same thing a genuine fault answers — so every failed disconnect re-reads before it shows anything, and a card left over from another tab does not survive the round trip.

WhatValue
A link's life24 hours from creation, or one successful use
Platforms a link can connect3
Assets per platformOne WhatsApp number, one Instagram account, one TikTok account; any number of Facebook pages; one web widget
A tab away before returning re-reads60 seconds

The codes the page has a sentence for:

CodeWhenWhat the page does
PlatformOperationLinkInvalidRedirectURLA redirect the API will not takeSays a redirect must be an https:// address with a host — though the app builds its own, and sends none where it cannot build one
NoConnectedContactScopeForPlatformRefresh access with nothing of that platform connectedSays there is no access to refresh
PlatformNotSupportedForOperationLinkA platform outside the threeSays the platform does not take a link
PlatformOperationLinkNotFoundA link that is not active any moreTreats it as already gone and re-reads
CannotDisconnectWidgetScopeDisconnecting the web widgetNever draws the control; the code arrives nested
ContactScopeDoesNotExistDisconnecting a channel that is already disconnectedTreats it as already gone and re-reads
ContactScopeAlreadyConnectedA second asset of a platform that already holds oneSays something on this platform is already connected to the bot
InternalServerErrorA scope the bot does not have — and a genuine faultRe-reads; the two cannot be told apart
NotEnoughPermissionsThe role lacks Configure: EditSays the role cannot manage channels on this bot
UnauthorizedThe token was rejectedSays to rotate it and reload

ContactScopeDoesNotExist is matched as a raw string, because the API answers it and the bundled DefinedErrorCode enum does not carry it.

On this page