Skip to content
ChatfuelSDK
Modules

Broadcasts

WhatsApp campaigns over the flow API — a campaign is a flow, the four statuses, the audience as a segment, the schedule in the bot's zone, and the template catalog you cannot write to.

What it is

There is no campaign entity on the Chatfuel API. A campaign is a flow holding two blocks: an entry point that carries the audience and the schedule — a WhatsAppOneTimeNotificationBlock for "send now", a WhatsAppScheduledMessageBlock for "later" and "on a repeat" — connected to a WhatsAppTemplateBlock that carries the message. One pair mutation creates both blocks and the connection; every setter afterwards is keyed by one of the three ids on that pair. The module reads every flow on the bot, keeps the ones shaped like that, and derives a campaign record from each. The dashboard's flow builder shows the same flows.

Nothing in the API writes a WhatsApp template. Templates are written and submitted in WhatsApp Manager and read here; the Templates tab is that catalog with each template's Meta status, a refresh that asks Meta for the latest, and a door to WhatsApp Manager.

Installed as

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

Neither hidden nor railHidden; it sits in the rail's growth group beside Publishing. The two recommendations are the audience's two ends: contacts is where the attributes an audience is built from are edited, and channels is where the WhatsApp number this module sends from gets connected — without a number the module shows the connect state on every screen.

Routes and views

The view is a path segment; everything else is a query parameter. The campaign list is the default, so it has no segment of its own.

PathWhat it shows
/broadcastsThe campaign list, with a panel beside it for the campaign ?c= names.
/broadcasts/compose?c=<flow>The composer over one draft: name, message, audience, schedule, review.
/broadcasts/templatesThe catalog on the bot's number, with Meta status.
ParameterWhat it does
cThe campaign — a flow id — whose panel or composer is open.
stepComposer only: name, message, audience, schedule or review. Absent means the first step not yet done.
statusList only: draft, scheduled, sending or sent. Absent means every status.
qThe search box, on the list and on the catalog.
tCatalog only: the template whose preview is open.

An unknown value falls back in silence, and defaults are left out of what is written. ⌘K opens the command palette anywhere in the module and ? the keyboard sheet.

The model

Four statuses, derived on every read. The entry point's element carries a status (Draft, Live, Paused, Finished) and the block an isEntryPointEnabled bit, and the campaign's status is read off both against the clock:

KindElementBitCampaign
One-timeDraft or Pauseddraft
One-timeLivesending
One-timeFinishedsent
ScheduledFinishedsent
Scheduledanyoffdraft
Scheduledanyon, first send ahead or repeatingscheduled
Scheduledanyon, once and the time has passedsending, until the next read says Finished

There is no paused state: blockDisableEntryPoint puts a scheduled element back to Draft, so "take off the schedule" makes a draft with everything kept. A one-time pair is born with its bit on and the bit cannot be turned off — the element's status is what counts. A row that is sending is re-read every ten seconds until it is not; a one-shot that is sending by the clock alone is re-read for an hour past its time, and Refresh after that.

Arming is silent when it fails. blockEnableEntryPoint re-validates the campaign and, when any verdict stands, answers the flow unchanged with the bit still off and no error. The module reads the bit back and shows the verdicts rather than assuming. Verdicts are data — blockElements[].errors[].code, snake_case, recomputed on every write — and each is printed on the review step under the step that fixes it. A thrown refusal sits at extensions.errors[0].extensions.code, one level down through the router; an unregistered one arrives as a bare InternalServerError, which is what every write to a live schedule answers. So the module disarms before it writes a time, and arms again on confirm.

Times are instants; weekdays are UTC. The first send goes up as an ISO instant and comes back as a Z string. A repeat's weekdays are stored normalised to UTC, so a Monday 01:00 campaign on a bot three hours east of Greenwich is stored as Sunday. The module shifts the list by the bot zone's day offset at the first send on the way out and on the way in, and sends the corrected list with every first-send-time write while the stored list is non-empty. The order of writes is the server's: repeat type, then its list, then the first send time last. The zone is the bot's; an operator's own zone is the fallback when the bot has none.

The audience is a segment. The entry point holds a SegmentInput with client-minted UUIDs; "everyone" is a segment with no filters and is valid as it stands. The recipient count is contactsTotalCount(platforms: [whatsapp], segment) with the very same segment. The builder writes two levels — the segment's own filters and one nested segment per filter slot — and reads three, keeping whatever it cannot show (a tag, a stored segment, a date strategy, a third level) as passthrough that is re-sent as read. Writes and counts are debounced together and the count is epoch-guarded, so a figure never describes a filter that has since changed.

An unknown attribute name in a template parameter creates the attribute. {{Attribute name}} in a blank becomes a reference the server parses; a name the bot does not have is accepted and a custom attribute is created for it, with no API to delete one. The composer's Insert field offers only botAttributes(platforms: [whatsapp]), and text typed by hand is sent as text. In an audience filter an unknown name is harmless — it selects nobody.

A block never changes type, and there is no clone. "Send now" on a scheduled draft creates the other pair on the same flow, replays the template and the audience onto it, and deletes the old entry point; the old template block cannot be deleted and stays in the flow, unconnected. Duplicate is a new flow with the source replayed onto it — template, every filled blank, media by file id, the segment with fresh ids, the repeat type and its list, never the first send time. Neither is one operation, and a partial failure keeps what was made and lists what is missing.

There are no delivery figures. WhatsAppTemplateBlock.stats answers a server error on every read, so the module never selects it. The only count the API has is sentToContactsCount on a finished one-time element, and it is sometimes null.

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.

Nothing of its own. The number it sends from is connected in the Channels module, and the module asks for the template catalog only once the bot has one.

Permissions

ObjectActionRequired for
FlowsViewListing campaigns — every campaign is a flow, read through the bot's flow groups.
FlowsEditCreating, filling, scheduling, sending, taking off the schedule and deleting. Every write is a flow-builder block mutation, and the server checks this, not Broadcasting.
BotViewThe template catalog, the bot's time zone and the connected number.
PeopleViewThe attribute catalog behind the audience builder and Insert field, and the recipient count.

Limits

The five time setters refuse while the campaign is armed, with a bare server error that names nothing; the module disarms first. A one-time campaign that has started is immutable: send and segment both answer WhatsAppOneTimeBroadcastAlreadyStarted, and the module treats that as "already sent" and re-reads. The catalog ignores paging and answers the whole list. Never call deleteFlowGroup — it deletes every flow in the group, campaigns included.

The module's own ceilings:

Days between repeats1 to 365 — the server refuses above 1000 with a bare error
Dates in a repeat500
Copy code15 characters
Text blank1,024 characters
Link parameter2,083 characters
Conditions in an audience20
Groups in an audience10
Campaign name120 characters
Wait before a filter change is written and counted400 ms
A sending row re-readevery 10 seconds, for an hour past a one-shot's time
Catalog read after a Meta refreshat 3 s and again at 10 s

On this page