Skip to content
ChatfuelSDK
Modules

Deals

A sales pipeline over contacts — the three views, the six fixed stages, and why a deal is a contact rather than a record of its own.

There is no Deal entity in the Chatfuel API. A deal is a Contact whose salesStageV2 is set, so this module is three views over your contacts grouped by six fixed stages — and everything that looks like a deal field (amount, close date, company) is a custom contact attribute the module writes by name.

That is also why deals requires contacts: the record you are dragging around the board is the same record the CRM edits.

Installed as

Wizard iddeals
Flag--modules deals
SelectionDefault — --yes installs it
Requirescontacts
Recommends
Skillchatfuel-deals

Routes and views

The view is a path segment; everything else is a query parameter. board is DEFAULT_VIEW, so it has no segment of its own.

PathWhat it shows
/dealsThe kanban board: six columns, one query per column, drag to change stage.
/deals/tableThe same deals as rows — search over name and phone, a stage subset, attribute conditions, sortable columns.
/deals/forecastDeals per stage over a window, win rate, pipeline and weighted pipeline, CSV export, saved views.

The parameters the module owns:

ParameterValue
dealContact id of the open deal. Opens beside the view — a drawer below 1280px, an inline second column above it.
assigneeany, unassigned, ai, or u:<UserAccountID> for a person.
qSearch text.
stageComma list of SalesStageV2 members.
unread1, or absent.
sort<attributeName>:asc or :desc.
collapsedComma list of the columns collapsed on the board.
densitycomfortable (the default) or compact.

The filter keys are shared across the three views, but the board cannot express most of them: its query takes an assignee and nothing else, which is why the board's only filter control is assignee. The table is where the rest apply.

Attribute predicates are deliberately not in the URL — they are unbounded in size and would make a link unshareable, so they live in saved views instead. Neither is a default: a mount that wrote the whole schema back would turn every shared link into six parameters of noise. A ?view= from an older link is still read once and dropped on the next write, and an unrecognised value anywhere renders the default rather than throwing.

The model

Six stages, fixed by the enum, in board order:

SalesStageV2: New | Sorting | Ready | WorkingOn | Won | Lost

WorkingOn renders as "Working on". The first four are OPEN_STAGES — everything still in play, which is what a pipeline rollup covers.

The seven deal fields are custom contact attributes, and contactAttributeUpdate is documented "Create or update custom contact attribute" — writing a value is what creates the attribute. lib/dealFields.ts is the whole convention in one file:

FieldAttribute nameKind
Amountdeal amountPlain decimal, no symbol and no separators
Currencydeal currencyISO-4217, upper case; EUR when a deal has an amount and no currency
Close datedeal close dateMillisecond timestamp
Companydeal companyText
Probabilitydeal probabilityBare integer per cent
Sourcedeal sourceText
Lost reasondeal lost reasonText

Amount and close date are CARD_FIELDS: shown on the card and summed in the column rollup. The rest live in the panel and in the table.

There is no rename mutation for a custom attribute. Change an attributeName in lib/dealFields.ts after data exists and the old values are orphaned — and nothing errors: a name that does not exist yet is silently omitted from contact.attributes(names:) rather than reported, so the field reads empty on every deal. Add the old name to that field's aliases instead, which is what they are for.

The one timestamp the API keeps per deal is lastSalesStageUpdateTime, and it is the last transition. The card's ageing bar is therefore "days since it last moved", never "days in this stage" — two days in New, three in Sorting, five in Ready, fourteen in Working on. Won and Lost have no threshold at all: a closed deal sitting still is not rot.

Configuration

VariableNotes
CHATFUEL_TOKENSecret. Server-side only.
VITE_CHATFUEL_WORKSPACE_IDThe wizard's workspace step writes it.
CHATFUEL_API_BASEDefaults to https://panel.chatfuel.com.

Nothing here is specific to this module. Environment variables has what each one is and which side reads it.

Permissions

ObjectActionRequired for
PeopleViewReading the deals board
PeopleEditMoving cards between stages, editing deal fields, name, note and owner

Limits

A segment cannot be narrowed by stage. SegmentInput has no sales-stage predicate. That one gap costs two things: the table's attribute engine loses deal isolation (it returns contacts, and a stage filter can only be applied client-side to the rows that loaded), and the segment CSV export is every contact on the bot rather than the deals on screen. The other export start, csvContactExportStartByIDsList, means "these deals" — and can only carry the ids that are loaded, which is why the forecast offers to load the rest beside the export.

There is no aggregation of any attribute — no SUM, no AVG. Every money figure is a client-side sum over the loaded rows and always renders its coverage (60 of 128). Mixed currencies refuse to sum rather than print a wrong total.

Four analytics are absent from the data, not from the roadmap. With one timestamp per deal and no stage-change history: no time-in-stage, no funnel conversion, no sales velocity or average days to close, no cohorts. A window selects deals whose last update falls in it, which is not the set that entered the pipeline in it.

Within-column order cannot be persisted. The board's sort is fixed to lastSalesStageUpdateTime desc and contactDealsConnection has no orderBy, so a drop indicator anywhere but the top of the target column would be a lie the next render corrects.

There is no bulk mutation. A multi-card drag is one contactSetSalesStage per card, a partial failure is an ordinary outcome, and past 25 cards the drag is refused rather than firing that many requests at a rate-limited bot.

Only attribute-backed columns sort. The server orders by an attribute name; stage, owner, unread and last message have none. On the attribute engine every custom attribute reports dataType: string, so orderBy sorts as text — "9" comes after "1000".

Saved views are per signed-in user. setUserStorageItem is the only persistence this API offers a client, so a teammate on the same bot sees none of yours.

The module's own ceilings, each picked against something the API does:

Cards in a column page20
Pages the board auto-loads before a button3
Rows in a table page25
Cards in one drag25
Undo window60 seconds, one move deep
Saved views50
Saved view name60 characters
Search kept in a saved view200 characters
Conditions in a saved filter20
Values on a condition20

On this page