@funnelchat-hq/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +530 -2
- package/dist/cli.js +2797 -0
- package/package.json +62 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Funnelchat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,531 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @funnelchat-hq/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Funnelchat CLI. **Connect a WhatsApp number, send a message and watch it get read**, in three commands, without writing code or a webhook:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --global @funnelchat-hq/cli
|
|
7
|
+
|
|
8
|
+
funnelchat login # 1. paste your API key; it is not shown
|
|
9
|
+
funnelchat connect # 2. open the link it prints and scan the QR with WhatsApp
|
|
10
|
+
funnelchat send 5491122334455 "Hello from Funnelchat" # 3. and watch it: queued → sent → delivered → read
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Sending to 5491122334455 from Support (5491100000002)…
|
|
15
|
+
queued
|
|
16
|
+
sent after 1 s
|
|
17
|
+
delivered after 2 s
|
|
18
|
+
read after 14 s
|
|
19
|
+
Message 98765 to 5491122334455: read 14 s after sending.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The phone does not need to be a contact yet: the send creates it. Send it to your own phone to see it arrive.
|
|
23
|
+
|
|
24
|
+
**Handing it to an AI agent?** Agents are good at running commands. Two things help them: `funnelchat connect --no-wait` prints the link and ends at once (the agent shows it to you, and `funnelchat numbers list` says when the number is connected), and `--json` on any command gives one JSON document to read.
|
|
25
|
+
|
|
26
|
+
The same key also **follows your WhatsApp groups** (`funnelchat groups`), **exports your conversations and your contacts** to files you can verify, and checks your numbers, templates and MACs:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
funnelchat groups # the groups of your number, as WhatsApp has them
|
|
30
|
+
funnelchat contacts export --output-dir ./my-contacts
|
|
31
|
+
funnelchat conversations export --since 2026-10-01 --output-dir ./october
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> **Status: v0.** The public API is at v0 and may change. The CLI uses only what the API has in production; what gets added later is in [Coming next](#coming-next). The API reference is on the [developer portal](https://developers-portal.funnelchat.app/docs/).
|
|
35
|
+
|
|
36
|
+
## What it does and what it does not
|
|
37
|
+
|
|
38
|
+
- **It writes only when you ask it to.** `connect` creates a connect link, `send` sends a message (and creates the contact if the phone is new) and `groups follow` imports a group into your account. Every other command only reads: each call it makes is a `GET`.
|
|
39
|
+
- **A write is sent once.** The CLI never repeats it on its own; if the network fails halfway, it says the write may have arrived, so you can check before repeating it.
|
|
40
|
+
- **It exports by paging through the public API**, because the API has no server-side export. A large account takes a while: the CLI goes at a low pace on purpose, so it does not slow down your panel.
|
|
41
|
+
- **It never stores or shows your key** anywhere except your configuration file, if you use `funnelchat login`.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
You need Node 18 or later.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install --global @funnelchat-hq/cli
|
|
49
|
+
funnelchat --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or without installing it: `npx @funnelchat-hq/cli --version`.
|
|
53
|
+
|
|
54
|
+
### An export example
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
funnelchat login # paste your key; it is not shown
|
|
58
|
+
funnelchat whoami # which account it belongs to
|
|
59
|
+
funnelchat contacts export --output-dir ./my-contacts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It leaves `my-contacts/contacts.csv` and `my-contacts/manifest.json`, with the SHA-256 of every file.
|
|
63
|
+
|
|
64
|
+
### From the source
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/Funnelchat20/funnelchat-cli.git
|
|
68
|
+
cd funnelchat-cli
|
|
69
|
+
npm install
|
|
70
|
+
npm run build
|
|
71
|
+
npm link # makes the `funnelchat` command available in your terminal
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Without `npm link`, run `node dist/cli.js` instead of `funnelchat`.
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
The CLI uses an API key of your account: `<id>|fc_live_...`, complete, as the **Developers** screen shows it when you create it. The permission each command needs:
|
|
79
|
+
|
|
80
|
+
| Command | Permission |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `connect` | `accounts:operate` (WhatsApp numbers → Create, connect and delete) and `accounts:read` to wait for the number |
|
|
83
|
+
| `send` | `conversations:send`; to a phone that is not a contact yet, `conversations:write` (it creates the contact); `conversations:read` to watch the status; `accounts:read` to pick the number without `--device` |
|
|
84
|
+
| `groups` | `communities:read`; `accounts:read` without `--device` |
|
|
85
|
+
| `groups follow` | `communities:operate` (Groups and campaigns → Manage groups); `accounts:read` without `--device` |
|
|
86
|
+
| `contacts export`, `conversations export`, `templates`, `mac status` | `conversations:read` |
|
|
87
|
+
| `whoami`, `numbers` | `accounts:read` |
|
|
88
|
+
|
|
89
|
+
For the first use, a key with **WhatsApp numbers** (view and connect) and **Contacts and conversations** (view, edit and send) is enough.
|
|
90
|
+
|
|
91
|
+
There are two ways to give it the key:
|
|
92
|
+
|
|
93
|
+
**With an environment variable**, for scripts and servers:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export FUNNELCHAT_API_KEY='<id>|fc_live_...' # in quotes: it contains a |
|
|
97
|
+
funnelchat whoami
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**With `funnelchat login`**, which saves it in your configuration file with `0600` permissions (only you can read it):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
funnelchat login
|
|
104
|
+
# Paste your Funnelchat API key (it is not shown) and press Enter:
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Key saved in the "default" profile (/Users/ana/.config/funnelchat/config.json, 0600 permissions).
|
|
109
|
+
Account: Acme (hello@acme.example)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Before saving it, `login` checks it against `GET /api/v1/me`: if the API rejects it, it is not saved. It also reads it from standard input (`funnelchat login < key.txt`). The CLI **never** takes the key from a command-line option, so it does not end up in your shell history.
|
|
113
|
+
|
|
114
|
+
**Which key it uses.** With `--profile`, that profile's. Otherwise `FUNNELCHAT_API_KEY`, if it is set. Otherwise the active profile's (the last one you ran `login` with, or `FUNNELCHAT_PROFILE`).
|
|
115
|
+
|
|
116
|
+
**Profiles.** For several accounts or environments:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
funnelchat login --profile client-b
|
|
120
|
+
funnelchat login --profile testing --staging
|
|
121
|
+
funnelchat profiles # lists the profiles, never the keys
|
|
122
|
+
funnelchat profiles use client-b # chooses the active one
|
|
123
|
+
funnelchat --profile testing whoami
|
|
124
|
+
funnelchat logout --profile testing # deletes that profile's key
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**The base URL.** By default, `https://api.funnelchat.com`. `--staging` uses `https://api-staging.funnelchat.com`; `--base-url` (or `FUNNELCHAT_BASE_URL`) any other, always `https`. `http` is accepted only for your own machine, for example the [sandbox](https://github.com/Funnelchat20/funnelchat-sandbox):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
FUNNELCHAT_API_KEY='1|fc_live_sandbox' funnelchat --base-url http://127.0.0.1:4010 contacts export
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**The configuration file** is at `~/.config/funnelchat/config.json` (or in `$XDG_CONFIG_HOME/funnelchat/`, or in `%APPDATA%\funnelchat\` on Windows; `FUNNELCHAT_CONFIG_DIR` changes it). If other users of your machine can read it, the CLI warns you.
|
|
134
|
+
|
|
135
|
+
## Commands
|
|
136
|
+
|
|
137
|
+
Every command takes `--json` for use from scripts and `--help` to see all its options.
|
|
138
|
+
|
|
139
|
+
### `funnelchat connect`
|
|
140
|
+
|
|
141
|
+
Connects a WhatsApp number to your account by QR code. It creates a connect link (`POST /api/v1/connect-links` with `type: "qr"`), prints it, and waits until the number is connected.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
funnelchat connect
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Open this link, tap the button and scan the QR code with WhatsApp on the phone you are connecting (Linked devices → Link a device):
|
|
149
|
+
|
|
150
|
+
https://…
|
|
151
|
+
|
|
152
|
+
It works once and expires at 2026-11-05T21:00:00+00:00; after tapping the button there are 10 minutes to scan the QR.
|
|
153
|
+
Whoever has the link can connect a number to your account: do not share it.
|
|
154
|
+
|
|
155
|
+
Waiting for the number to connect… (Ctrl+C to stop)
|
|
156
|
+
Connected: Support (5491100000002), id 4.
|
|
157
|
+
Send a first message: funnelchat send <phone> "Hello from Funnelchat"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Option | What it does |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `--no-wait` | Prints the link and ends, without waiting. For scripts and agents: `funnelchat numbers list` then shows when the number is connected. |
|
|
163
|
+
| `--timeout <seconds>` | How long to wait for the number. By default, 900 (15 minutes). |
|
|
164
|
+
|
|
165
|
+
- **The link** is a page on Funnelchat's domain. Opening it does not use it; tapping its button creates the number and shows the QR, which renews itself every 5 seconds. Treat it as a secret: it comes only in this answer, and whoever has it can connect a number to your account.
|
|
166
|
+
- **How it knows:** it lists your numbers before creating the link and then every 5 seconds (`GET /api/v1/devices`), and stops at the first one that was not connected before. Each listing counts toward the daily cap of accounts (about 180 calls in 15 minutes, of 10,000 a day).
|
|
167
|
+
- **With `--json`**, the link goes to stderr as soon as it exists, and stdout gets one document at the end, with the link and the number.
|
|
168
|
+
- **No QR in the terminal:** the page shows the QR that WhatsApp scans.
|
|
169
|
+
- If nobody scans the QR within 10 minutes of tapping the button, the link fails and the number it created is deleted: run `funnelchat connect` again. If the CLI stops waiting before (`--timeout`, exit code `8`, or Ctrl+C), the link still works until it expires.
|
|
170
|
+
|
|
171
|
+
### `funnelchat send <phone> <text>`
|
|
172
|
+
|
|
173
|
+
Sends a text message to a phone and shows its status live, until it is `read`, it `failed` or `--timeout` ends.
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
funnelchat send 5491122334455 "Hello from Funnelchat"
|
|
177
|
+
funnelchat send +54 9 11 2233-4455 "Hello" --device 4 --timeout 300
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Option | What it does |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `--device <id>` | The number it sends from. By default, the only connected number of your account; with several, the CLI asks for this. |
|
|
183
|
+
| `--timeout <seconds>` | How long to watch the status. By default, 90. `0` sends and does not watch. |
|
|
184
|
+
|
|
185
|
+
- **The phone**, with its country code. Spaces, dashes, dots, parentheses and a leading `+` are dropped. It does not need to be a contact yet: the send creates it (`POST /api/v1/message/send-message`, with `device_id`, `phone` and `text`).
|
|
186
|
+
- **The status:** `queued` (in line, not sent yet), `sent`, `delivered` and `read`, or `failed` with the reason. The CLI reads it with `GET /api/v1/contacts/{contact}/messages/{message}`, 1 second after each change and then less and less often (2 s, 4 s… up to 10 s). If the recipient turned read receipts off, it stays `delivered`.
|
|
187
|
+
- **Exit codes:** `0` when it is read, or still on its way when `--timeout` ends; `6` when it failed (`code` `message_failed`, with the reason in `--json`). Ctrl+C stops watching (`130`); the message was already sent.
|
|
188
|
+
- **On an official number** (WhatsApp Cloud API), free text only goes out within 24 hours of the contact's last message, so the first message to a new contact has to be a template: the API answers `422 window_closed` and nothing is sent. A number connected by QR has no such window.
|
|
189
|
+
- **Rejections say what happened in plain words,** and that nothing was sent when the API guarantees it: `window_closed`, `recipient_mismatch`, `antiblock_*`, `device_unavailable`, `403` (the permission missing) and `429` (how long to wait). A send is never repeated on its own.
|
|
190
|
+
- **With `--json`**, one document with the message, every status with the seconds it took, and `outcome`: `read`, `timeout`, `unknown` (the provider never confirmed it: it will not change) or `not_watched`.
|
|
191
|
+
|
|
192
|
+
### `funnelchat groups`
|
|
193
|
+
|
|
194
|
+
The WhatsApp groups of a number connected by QR, as WhatsApp has them, and which ones you follow.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
funnelchat groups
|
|
198
|
+
funnelchat groups --device 4
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
uid name followed
|
|
203
|
+
120363000000000001@g.us Launch Nov yes
|
|
204
|
+
120363000000000002@g.us Clients VIP no
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- **Live:** `GET /api/v1/devices/{device}/groups` reads them from WhatsApp, and reuses the list for up to a minute per number: a group just created can take that long to show.
|
|
208
|
+
- **The number:** `--device`, or the only number of your account connected by QR.
|
|
209
|
+
|
|
210
|
+
### `funnelchat groups follow <group>`
|
|
211
|
+
|
|
212
|
+
Follows a group: imports it to your account, as importing it from the panel does. From then on, who joins and who leaves arrives as the `group_participant.joined` and `group_participant.left` webhook events.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
funnelchat groups follow 120363000000000002@g.us
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Following "Clients VIP" (120363000000000002@g.us).
|
|
220
|
+
Who joins and who leaves arrives as the group_participant.joined and group_participant.left webhook events.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- `<group>` is the `uid` that `funnelchat groups` shows. A group already followed says so, and nothing changes.
|
|
224
|
+
- **Only numbers connected by QR** (`422 provider_unsupported` otherwise), and up to 5 groups per number per minute (`429 import_rate_limited`). If the number is not an admin of the group, the CLI warns you: it can follow it, but not send messages to it or manage it.
|
|
225
|
+
- `POST /api/v1/groups/{group}/import` with `device_id`; it needs `communities:operate` (Groups and campaigns → Manage groups).
|
|
226
|
+
|
|
227
|
+
### `funnelchat whoami`
|
|
228
|
+
|
|
229
|
+
Which account the key belongs to, without showing the key.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
funnelchat whoami
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
Account: Acme (hello@acme.example)
|
|
237
|
+
id 42 · role owner · time zone America/Argentina/Buenos_Aires · language es
|
|
238
|
+
Key: from the FUNNELCHAT_API_KEY variable (not shown)
|
|
239
|
+
Base URL: https://api.funnelchat.com
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
It shows only these fields of `GET /api/v1/me`: id, name, email, role, time zone, language and the number and agent limits. That route also returns a session token: the CLI never shows it, not even with `--json`.
|
|
243
|
+
|
|
244
|
+
### `funnelchat numbers list`
|
|
245
|
+
|
|
246
|
+
Your numbers, with their connection and what the CLI can export or read for each one. Useful to choose what to export.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
funnelchat numbers list
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
id alias number connection connected health what the CLI reads
|
|
254
|
+
3 Sales 5491100000001 official yes AVAILABLE contacts, conversations, templates, health and limit
|
|
255
|
+
4 Support 5491100000002 QR yes - contacts, conversations
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The CLI works out "what the CLI reads" from the connection type (`provider`): official numbers (WhatsApp Cloud API) have templates and health; QR ones do not. With `--json` it comes as `cli_capabilities`. The `capabilities` field of each number is something else: what the API can do with it (message types, groups, the statuses it reports), exactly as `GET /api/v1/devices` returns it.
|
|
259
|
+
|
|
260
|
+
### `funnelchat numbers health`
|
|
261
|
+
|
|
262
|
+
The health of your official numbers: state, quality, daily messaging limit and throughput, from the last data Funnelchat stored (it does not query Meta at that moment).
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
funnelchat numbers health # every official number
|
|
266
|
+
funnelchat numbers health 3 # one
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
id state quality limit per day throughput updated stale
|
|
271
|
+
3 AVAILABLE GREEN TIER_1K 1,000 STANDARD 2026-10-16T10:00:00+00:00 no
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### `funnelchat contacts export`
|
|
275
|
+
|
|
276
|
+
All your contacts, with their tags and their custom fields, as CSV (the default) or JSONL.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
funnelchat contacts export --output-dir ./contacts
|
|
280
|
+
funnelchat contacts export --format jsonl --tag 4001 --created-from 2026-10-01 --created-to 2026-10-15 -o ./new-clients
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
Exporting contacts to /Users/ana/contacts
|
|
285
|
+
contacts: 100 of 436
|
|
286
|
+
…
|
|
287
|
+
Export complete: 436 contacts in /Users/ana/contacts/contacts.csv.
|
|
288
|
+
Manifest: /Users/ana/contacts/manifest.json (sha256 9f2c…)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Option | What it does |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `-o`, `--output-dir <dir>` | A new or empty folder. By default, `funnelchat-contacts-export-<date>`. |
|
|
294
|
+
| `--format csv\|jsonl` | CSV for spreadsheets (the default) or JSONL, one contact per line as the API returns it. |
|
|
295
|
+
| `--tag <id>` | Only contacts with that tag. It can be repeated (`--tag 1 --tag 2`) or comma-separated. |
|
|
296
|
+
| `--tag-mode any\|all\|exclude` | With several tags: any of them, all of them, or none of them. |
|
|
297
|
+
| `--created-from`, `--created-to` | Created between those dates (`YYYY-MM-DD`, both whole days). They go together: the API needs both. |
|
|
298
|
+
| `--only-with-phone` | Only contacts that can be messaged. |
|
|
299
|
+
| `--no-tags`, `--no-custom-fields` | Without tags or without fields: each one saves a call per contact. |
|
|
300
|
+
| `--resume` | Resumes an interrupted export. See [Resuming](#resuming). |
|
|
301
|
+
|
|
302
|
+
- **The CSV** has a column per contact field (`id`, `name`, `phone`, `email`, `whatsapp_id`, `country_code`, `created_at`, `lead_score`…), `tags` (the names, separated by `; `) and `tag_ids`, and a `custom_field:<name>` column for each custom field of your account. It has a UTF-8 BOM and CRLF line endings, so Excel shows accented characters correctly.
|
|
303
|
+
- **A cell that starts with `=`, `+`, `-` or `@`** (and is not a number) gets a `'` in front: your contacts write their own names, and this way your spreadsheet does not run them as formulas.
|
|
304
|
+
- **It takes longer with tags and fields:** the contacts listing does not include them, so the CLI makes two more calls per contact. At the default pace (1 per second), 1,000 contacts with everything are about 2,010 calls, about 35 minutes. If you do not need them, `--no-tags --no-custom-fields` brings it down to 10 calls.
|
|
305
|
+
- **The order** is by ascending `id`.
|
|
306
|
+
|
|
307
|
+
### `funnelchat conversations export`
|
|
308
|
+
|
|
309
|
+
The messages of every conversation (each contact's history), in JSONL parts, with `conversations.jsonl` and a manifest.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
funnelchat conversations export --output-dir ./conversations
|
|
313
|
+
funnelchat conversations export --since 2026-10-01 -o ./october
|
|
314
|
+
funnelchat conversations export --contact 2001 --contact 2002 -o ./two-clients
|
|
315
|
+
funnelchat conversations export --device 3 --part-size 10000 -o ./sales
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
Exporting conversations to /Users/ana/october
|
|
320
|
+
conversations: 340
|
|
321
|
+
conversation 1 of 340 (contact 2001): 128 messages · total 128
|
|
322
|
+
…
|
|
323
|
+
Export complete: 340 conversations and 21,450 messages in 1 part, in /Users/ana/october.
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Option | What it does |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `-o`, `--output-dir <dir>` | A new or empty folder. By default, `funnelchat-conversations-export-<date>`. |
|
|
329
|
+
| `--since <date>` | Conversations and messages **created or changed** since that date. `YYYY-MM-DD` means from 00:00 UTC; it also takes a date and time with a zone (`2026-10-01T09:00:00-03:00`). |
|
|
330
|
+
| `--contact <id>` | Only that contact's conversation (the API's `id`). It can be repeated or comma-separated. |
|
|
331
|
+
| `--device <id>` | Only the conversations of that number, and of them only its messages. |
|
|
332
|
+
| `--part-size <n>` | Messages per part. By default, 50,000. |
|
|
333
|
+
| `--resume` | Resumes an interrupted export. See [Resuming](#resuming). |
|
|
334
|
+
|
|
335
|
+
What ends up in the folder:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
october/
|
|
339
|
+
├── conversations.jsonl one conversation per line, as GET /api/v1/conversations returns it
|
|
340
|
+
├── messages-00001.jsonl one message per line, as GET /api/v1/contacts/{contactId}/messages returns it
|
|
341
|
+
├── messages-00002.jsonl
|
|
342
|
+
└── manifest.json
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
- **The order:** conversations in the order of `conversations.jsonl`, from the oldest activity to the newest; within each one, from the newest message to the oldest (that is how the API returns them).
|
|
346
|
+
- **A part closes at the end of a page**, so it can go over `--part-size` by up to 100 messages.
|
|
347
|
+
- **`--since` filters by modification date**, not by send date: an old message whose status changed after that date is included too. The API has no "until" filter.
|
|
348
|
+
- **`--device`** picks the conversations on the API side (`deviceId`); the CLI filters the messages, because the API does not filter messages by number.
|
|
349
|
+
- **If a contact is deleted** during the export, it is skipped and counted in the manifest (`vanished_contacts`).
|
|
350
|
+
|
|
351
|
+
### `funnelchat templates list`
|
|
352
|
+
|
|
353
|
+
Your WhatsApp templates, with their status and their number.
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
funnelchat templates list --device 3 --status APPROVED
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
id name language category status number
|
|
361
|
+
45 welcome es UTILITY APPROVED 3
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### `funnelchat mac status`
|
|
365
|
+
|
|
366
|
+
How many monthly active contacts (MACs) you have used in the period, the limit and how many are left. If your account has per-contact MAC enforcement, also how many contacts became inactive: the API does not let you send them messages with a key (`429 mac_lockout_24h`).
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
funnelchat mac status
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
MACs in the period: 120 of 1,000 (12 %) · 880 left
|
|
374
|
+
state normal · cap enforcement on
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Exports
|
|
378
|
+
|
|
379
|
+
> **Personal data.** What you export holds your contacts' personal data: names, phone numbers and messages. Keep it somewhere safe, do not share it unless you need to, and delete it when you no longer use it.
|
|
380
|
+
|
|
381
|
+
### Files you can verify
|
|
382
|
+
|
|
383
|
+
- **Nothing gets its final name until it is complete:** each file is written as `<name>.part` and only takes its name at the end.
|
|
384
|
+
- **A file is never overwritten:** the folder must be new or empty, and the final name is taken in a way that fails if someone created that file in the meantime.
|
|
385
|
+
- **The manifest** (`manifest.json`) says what was exported, from where and how to verify it. It never includes your key.
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"manifest_version": 1,
|
|
390
|
+
"kind": "conversations",
|
|
391
|
+
"generator": { "name": "@funnelchat-hq/cli", "version": "0.2.0" },
|
|
392
|
+
"api": { "base_url": "https://api.funnelchat.com", "spec": { "commit": "99928866…", "sha256": "0ffaad2…" }, "routes": [ … ] },
|
|
393
|
+
"started_at": "2026-10-16T12:00:00.000Z",
|
|
394
|
+
"finished_at": "2026-10-16T12:41:10.000Z",
|
|
395
|
+
"runs": 2,
|
|
396
|
+
"api_calls": 1873,
|
|
397
|
+
"options": { "contact_ids": [], "device_id": null, "since": "2026-10-01T00:00:00+00:00", "part_size": 50000 },
|
|
398
|
+
"range": { "since": "2026-10-01T00:00:00+00:00", "until": null, "observed_first_message_time": "2026-10-01 08:12:44", "observed_last_message_time": "2026-10-16 11:58:02" },
|
|
399
|
+
"counts": { "conversations": 340, "messages": 21450, "message_parts": 1, "vanished_contacts": 0 },
|
|
400
|
+
"files": [
|
|
401
|
+
{ "path": "conversations.jsonl", "rows": 340, "bytes": 182044, "sha256": "…" },
|
|
402
|
+
{ "path": "messages-00001.jsonl", "rows": 21450, "bytes": 9811230, "sha256": "…" }
|
|
403
|
+
],
|
|
404
|
+
"notes": [ "…" ]
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
To verify a part:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
shasum -a 256 october/messages-00001.jsonl # on Linux: sha256sum
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
`runs` says how many times it ran (1 plus one per `--resume`), and `observed_*_message_time` is the `time` field of the messages, as the API returns it (without a time zone).
|
|
415
|
+
|
|
416
|
+
### Resuming
|
|
417
|
+
|
|
418
|
+
If an export gets interrupted (Ctrl+C, the network, an API limit), what was exported up to the last complete page is saved and the CLI tells you how to continue:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
error: Could not connect to the Funnelchat API (api.funnelchat.com).
|
|
422
|
+
Check your connection and the base URL, and try again.
|
|
423
|
+
What was exported up to the last complete page is saved. To continue from there:
|
|
424
|
+
funnelchat contacts export --output-dir ./contacts --resume
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
funnelchat contacts export --output-dir ./contacts --resume
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- **No row is repeated:** the state is saved after every page, and on resuming anything written after the last save is discarded.
|
|
432
|
+
- **With `--resume` there is no need to repeat the options:** the original export's are used. If you pass a different one, the CLI does not continue (so two different exports never mix in one folder). Resume it with the same key and the same base URL.
|
|
433
|
+
- **The state** lives in `<dir>/.funnelchat-export.json` while the export is not finished. It is deleted at the end.
|
|
434
|
+
- **If contacts are deleted during the export**, paging `GET /api/v1/contacts` by page number can skip some: compare `counts.contacts` with `counts.api_total_at_start` in the manifest.
|
|
435
|
+
|
|
436
|
+
## Being a good citizen with the API
|
|
437
|
+
|
|
438
|
+
- **Low pace:** at most 1 call per second, retries included. Change it with `--rate` (greater than 0 and up to 10; `0.5` is one every two seconds) or `FUNNELCHAT_RATE`. The API's per-minute limits are counted per user and your panel shares them: raise it carefully.
|
|
439
|
+
- **`429` and `503`:** the CLI waits exactly what `Retry-After` says and retries. If it asks for more than 60 seconds (`--max-wait` changes that), it stops with code `5` and tells you when it resets.
|
|
440
|
+
- **Server and network errors:** it retries with exponential backoff (1 s, 2 s, 4 s… up to 30 s), up to 4 times; also a `502 origin_unreachable` from the API's entry point. **It only retries reads**: a write (`connect`, `send`, `groups follow`) is sent once, and a `429` on it says how long to wait. A `500 edge_misconfigured` is not retried: it is a configuration problem on our side and waiting does not fix it.
|
|
441
|
+
- **Waiting without hurry:** `connect` lists your numbers every 5 seconds; `send` reads the message 1 second after each change and then less and less often, up to every 10 seconds.
|
|
442
|
+
- **The daily cap:** accounts and communities allow up to 10,000 calls per day per key, each on its own; conversations has no daily cap. The contacts and conversations exports and the message of `send` use only conversations; `whoami`, `numbers`, `connect` and picking the number without `--device` use accounts; `groups` uses communities. If the cap is reached, the CLI stops with a clear message and the reset time, and the export can be resumed.
|
|
443
|
+
|
|
444
|
+
## For scripts
|
|
445
|
+
|
|
446
|
+
**`--json`** on any command: the result goes to stdout as a single JSON document; progress and warnings go to stderr. An error is a JSON object on stderr:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{"error":{"code":"daily_cap_exceeded","message":"The daily cap of calls to accounts was reached for this key. It resets in 8 h 30 min (at 20:30 UTC on 2026-10-16).","hint":"Request id (X-Request-Id): 3f6c1a52-8d0e-4b7a-9c1e-5a2b7d4e9f10. Quote it, with the approximate time, if you report this to support.","exit_code":5,"status":429,"route":"GET /api/v1/devices","api_error":"daily_cap_exceeded","retry_after":30600,"request_id":"3f6c1a52-8d0e-4b7a-9c1e-5a2b7d4e9f10","resets_at":"2026-10-16T20:30:00.000Z"}}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**The request id.** Every answer of the API carries `X-Request-Id`, errors included. The CLI shows it in every error that came from the API (in `request_id` with `--json`) and on every answer with `--verbose`. If you report a problem, quote it together with the approximate time: with that id support finds the call. A network error has no id: the API never answered.
|
|
453
|
+
|
|
454
|
+
**Other global options:** `-q`/`--quiet` (no progress), `-v`/`--verbose` (every request to the API, with method, URL, status, timing and `X-Request-Id`; never the key).
|
|
455
|
+
|
|
456
|
+
The CLI asks the API for its messages in Spanish (`Accept-Language: es`), so the `message` the API returns, which the CLI appends to some errors, comes in Spanish. Decide by the exit code and by `code`, not by the text.
|
|
457
|
+
|
|
458
|
+
### Exit codes
|
|
459
|
+
|
|
460
|
+
| Code | What happened |
|
|
461
|
+
|---|---|
|
|
462
|
+
| `0` | All good. |
|
|
463
|
+
| `1` | An unexpected error. |
|
|
464
|
+
| `2` | Wrong usage: an invalid option or value, or `--resume` with options different from the original ones. |
|
|
465
|
+
| `3` | No key, a key the API rejects (`401`) or a key without the permission the route needs (`403`). |
|
|
466
|
+
| `4` | Not found in your account (`404`), or the route does not exist in the API (`404 route_not_found`). |
|
|
467
|
+
| `5` | An API limit the CLI does not wait for on its own: the daily cap or a long `Retry-After`. It can be resumed. |
|
|
468
|
+
| `6` | The API failed (`5xx`, `422`…) or could not be reached, after the retries; or the message `send` watched failed. |
|
|
469
|
+
| `7` | The output already exists: the CLI never overwrites a file. |
|
|
470
|
+
| `8` | `connect` waited `--timeout` and no number connected. |
|
|
471
|
+
| `130` | Interrupted with Ctrl+C. An export can be resumed. |
|
|
472
|
+
|
|
473
|
+
## What the API does not allow yet
|
|
474
|
+
|
|
475
|
+
The CLI uses only routes that exist in the public spec (`spec/routes-used.json` lists them and the generation fails if one is missing). What we looked for and is not there:
|
|
476
|
+
|
|
477
|
+
- **A server-side export** (like other platforms have): that is why the CLI pages.
|
|
478
|
+
- **A listing of every message of the account:** only the per-contact history exists, so the CLI walks the conversations and asks for each one's.
|
|
479
|
+
- **An "until" filter** on conversations and messages, or a filter by send date: only `updated_since`, by modification date.
|
|
480
|
+
- **A filter by number on messages:** with `--device`, the CLI filters.
|
|
481
|
+
- **Tags and fields in the contacts listing:** they are requested contact by contact.
|
|
482
|
+
- **A filter of contacts by modification date,** for an incremental contacts export: there is only the creation date.
|
|
483
|
+
- **Message metrics** (sent, delivered and read per day), neither for the account nor per template.
|
|
484
|
+
- **The key's own data** (its name and its permissions): `whoami` shows the account, not the key.
|
|
485
|
+
|
|
486
|
+
## Coming next
|
|
487
|
+
|
|
488
|
+
- **`templates metrics`**: what Meta reports for each template per day (sent, delivered, read, clicks and cost). It comes back to the CLI when the API has the route in production (`GET /api/v1/templates/{template}/metrics`, today in an undeployed change of conversations). It was removed on 2026-10-22 to stick to what the API already has.
|
|
489
|
+
|
|
490
|
+
## Where the spec comes from
|
|
491
|
+
|
|
492
|
+
`spec/funnelchat.json` is the portal's full unified spec (developers-portal `main` @ `99928866`, deployed as v0.6.3 on 2026-11-05: `generated/specs/funnelchat-completa.json`, which carries every operation, not only the ones the portal shows), built from the snapshots of accounts `v1.5.97` (`558332bcf`), conversations `v1.19.21` (`f2937a713`) and communities `54643bbd` (the app code of `v2.35.7`). `spec/procedencia.json` says so, with the file's `sha256`. Every route, parameter and body field the CLI uses is in it: `spec/pending.json` stays empty.
|
|
493
|
+
|
|
494
|
+
## Development
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
npm install
|
|
498
|
+
npm test # no network, no real keys
|
|
499
|
+
npm run lint && npm run typecheck
|
|
500
|
+
npm run build
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
To develop you need Node 20 or later (for the test tools); the CLI it builds runs on Node 18.
|
|
504
|
+
|
|
505
|
+
**The tests** use simulated answers, with the same shapes as the API (pages by number and by cursor). They cover paging, resuming, `429`s, that the key never shows up in any output or any file, that no file is overwritten, the manifest's SHA-256, and the first use: `connect` waiting for the number, and `send` watching the status until it is read.
|
|
506
|
+
|
|
507
|
+
**End to end against the sandbox.** With an already built checkout of [funnelchat-sandbox](https://github.com/Funnelchat20/funnelchat-sandbox) (`npm install && npm run build` in its folder), this test starts it in the same process, on a free port, and runs the CLI against it. It does not touch the sandbox's repository:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
FUNNELCHAT_SANDBOX_DIR=../funnelchat-sandbox npm run test:e2e
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Without that variable, `npm test` skips it.
|
|
514
|
+
|
|
515
|
+
**The spec.** It is copied and regenerated with a single script, which records where it came from in `spec/procedencia.json` and checks that every route, every parameter and every body field the CLI uses exists:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
npm run generate -- --spec ../developers-portal/generated/specs/funnelchat-completa.json # a new spec
|
|
519
|
+
npm run generate # regenerate
|
|
520
|
+
npm run generate:check # is it up to date?
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The portal's full spec is a build output git ignores, so a copy of it says where it came from on the command line, as funnelchat-node does: `--expect-sha256 <hex>`, `--source repository=… --source branch=… --source commit=… --source path=…` and `--domain <area>=<sha>@<branch>@<snapshot_at>` for each area. `spec/procedencia.json` marks those fields as declared.
|
|
524
|
+
|
|
525
|
+
`spec/pending.json` is for a route that is not in the snapshot yet; today it is empty. When a new snapshot has a route from there, the generation says it can be deleted.
|
|
526
|
+
|
|
527
|
+
**Publishing.** Push a `vX.Y.Z` tag on `main` that matches the version in `package.json`; GitHub Actions publishes it through npm Trusted Publishing (`.github/workflows/publish.yml`).
|
|
528
|
+
|
|
529
|
+
## License
|
|
530
|
+
|
|
531
|
+
MIT. See [LICENSE](LICENSE).
|