Skip to content
ChatfuelSDK
Modules

Ads Optimization

Conversion reporting for click-to-WhatsApp ads — event sets over one automation scope, the seven triggers, and the permission that decides whether any of it is delivered.

What it is

Nothing here is an ads dashboard. This module writes one setting on one automation scope: the event sets on bot.fuelyAutomations(scope: WhatsAppClickFromAds), which say which moments in a click-to-WhatsApp conversation are reported to Meta as conversions over the Conversions API — so Meta can optimize delivery towards the people who actually convert.

The surface is one screen: the sets on the left, the selected set beside them. And the ads a set claims are opaque strings that nothing ever checks.

Installed as

Wizard idads-optimization
Install itnpx @chatfuel/wizard --modules ads-optimization
Selected byDefault — --yes installs it
RequiresNothing, beyond the implicit core
Recommendsautomations, contacts
Skillchatfuel-ads-optimization

Neither hidden nor railHidden. The two recommendations are the surfaces the same data is seen from elsewhere: automations is the AI side of the very automations these sets live on, and contacts owns the properties and statuses two of the triggers fire on.

Routes and views

The path segment is not a view name here — it is the set. There is only one view.

PathWhat it shows
/ads-optimizationThe rail of sets, and the default set open beside it.
/ads-optimization/<automationID>The same, with that set open.
ParameterWhat it does
eThe event whose editor is open — an event id, or new for an empty one.

Both survive a reload, because both are places a link can be sent to. Nothing else about the surface is, so nothing else is in the address.

The model

A set is a FuelyAutomation. A bot has exactly one base set — isBase: true, and the API leaves its name null, which is why the module supplies one ("Default events for all ads") — and up to thirty custom ones. The base set carries no list of ads at all: the API strips filter settings from a base automation, and that absence is what makes it "everything". A custom set claims the ads it lists in FuelySettingListOfAds.adIDs and overrides the base for exactly those.

Every setting on a custom set either holds its own value or follows a parent, named by inheritsFrom; canInheritFrom says which parents it may be pointed at. Two consequences are worth building around. Saving an inherited setting takes a private copy — the set stops following, and for sendEventsToMeta every event id is regenerated, so ids read a moment ago are gone. And a write on a parent republishes its children: one edit arrives as several subscription events, one per affected set, so merge by id.

An event is a conversion name plus a trigger. The name is one of Meta's fourteen standard ones, or one of your own — at most fifty characters, reported to Meta verbatim, and refused if it is one of the fourteen spelled differently. bot.availableMetaConversionEventNames remembers the custom names this bot has already used; do not hard-code either list.

The seven triggers, in the order the picker offers them:

TriggerFires when
KeywordsThe contact sends a message that matches the words you list.
Contact propertyA property on the contact card comes to match your condition.
Contact statusThe contact reaches one of the statuses you pick — five of them, the same words the rest of the app uses for a sales stage.
Handed to a humanThe AI passes the chat to a teammate, or a teammate takes it.
BookingA booking is made in the conversation.
First messageThe contact sends the first message of the conversation.
A condition in wordsThe AI decides the conversation has met a condition you describe.

Every save rewrites the whole ordered list of events. There is no add-one and no delete-one mutation; order is stored and is part of the value. Leave an id out to delete that event, keep it to keep it, omit it entirely to add one. Two events reporting the same conversion on the same trigger are refused. The API also cleans the input before it validates — ad ids and keywords are trimmed, blanks dropped, repeats removed — so what you read back is not always what you sent.

Configured is not delivered. Nothing here is sent unless the bot's WhatsApp number can carry it, and the three ways it cannot all look identical on screen: no WhatsAppPhoneContactScope (no number at all), hasMetaConversionsAPIPermission: false (the stored Meta token lacks the permission), or accessLost: true (the number is no longer reachable with this token). Re-granting the permission is an interactive consent inside Chatfuel, which an app on the public API cannot run on somebody's behalf — so the module reads the state, shows an alert and links out. Without that alert, a fully configured set that reports nothing is indistinguishable from one that works.

Ad ids are opaque and unverified. Any string under sixty characters is accepted, no query turns an id back into an ad, and nothing reports that an id names an ad that was deleted, paused or never existed. So a typo saves happily. The module validates the shape — a bare 15-to-20 digit run — and never claims an id is real. It also takes ids out of whatever is in the clipboard, because nobody reads an id off a screen: they copy the Ads Manager address bar. When selected_ad_ids= is present it is trusted exclusively, since act= and business_id= are digit runs too and taking either for an ad id silently points a set at nothing.

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.

No secret and no database of its own. Every variable the app reads, and which side reads it, is on environment variables.

Permissions

ObjectActionRequired for
FlowsViewReading the event sets and their settings.
FlowsEditCreating, renaming and deleting sets, and writing ads and events.
BotViewReading the WhatsApp connection that conversions are delivered through.

The third one is why a token with full Flows rights can still show an empty delivery state: the permission that decides whether anything is sent is read off the bot, not off the automation.

Limits

The server refuses the whole list. Not the offending entry — the list. One bad twentieth event throws away nineteen good ones typed in the same session, which is why the module checks every ceiling before the write, using the same error codes the server would have answered with.

Two sets claiming the same ad is not reported anywhere. One of them silently loses. The whole list is in hand after one query, so the module builds the reverse index itself and names the rival set on the chip; nothing on the server will.

A contact-property condition the server dislikes is stored anyway. The save succeeds and the error comes back on the event in attributeConditionErrors, so read that array or the condition looks fine and does nothing.

Errors arrive one level down. The top-level message names a failed subgraph fetch; the code is at errors[0].extensions.errors[0].extensions.code. Read both levels or you will find nothing.

There is no history and no read-back. Conversions are sent and forgotten: no query reports what Meta received, what it accepted, or how an ad performed afterwards. That question is Ads Manager's. The API does expose ads — currentUser.metaAdAccounts and MetaAdAccount.ads(...) — and none of it is used here, for three reasons: it hangs off the signed-in person's Facebook login rather than off the bot, MetaAd cannot be looked up by id (resolving a set's ids means paging an account and matching client-side), and the insights carry no date range at all — one aggregate figure per ad, and no trend to draw.

Every write lands on the production bot immediately. There is no draft mode and no preview. Undo is one entry, sixty seconds, and a compensating write: a restored event comes back with a new id. And turning a set on can be refused by the plan — NotAllowedBySubscriptionFeatureSet on a plan without AI. A concurrent write on the same bot answers FuelyAutomationBeingEdited, which is a lock rather than a conflict and clears on its own.

The ceilings, each with the code the server answers when you pass it:

Custom sets per bot30FuelyAutomationScopeLimitReached
Set name200 charactersFuelyAutomationNameInvalid
Ads per set50FuelyListOfAdsTooManyEntries
One ad id60 charactersFuelyAdIDTooLong
Events per set20FuelySendEventsToMetaTooManyEvents
A conversion name of your own50 charactersFuelySendEventsToMetaCustomEventNameTooLong
A condition in words512 charactersFuelySendEventsToMetaConditionPromptTooLong
Keywords per event50FuelyKeywordsTooMany
One keyword50 charactersFuelyKeywordTooLong

On this page