@funnelchat-hq/sdk 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -40,16 +40,14 @@ Is the phone someone else's? Send them a page that shows the QR instead: `const
40
40
 
41
41
  ### 3. Send a message
42
42
 
43
- The contact goes first; then the text, from your number:
43
+ One call, from your number to a phone (with country code, digits only):
44
44
 
45
45
  ```ts
46
- const contact = await funnelchat.contacts.create({ phone: '5491100000000', name: 'Ana' }); // with country code, digits only
47
-
48
- const { message } = await funnelchat.messages.sendDirect({ device_id: device.id, contact_id: contact.id, text: 'Hi Ana!' });
49
- console.log(message.id, message.status); // 1002 'queued'
46
+ const { message } = await funnelchat.messages.sendDirect({ device_id: device.id, phone: '5491100000000', text: 'Hi Ana!' });
47
+ console.log(message.id, message.contact_id, message.status); // 1002 2001 'queued'
50
48
  ```
51
49
 
52
- The answer is the queued message: accepted is not delivered yet. With an official number, free text only goes within 24 hours of the contact's last message; after that, an [approved template](#templates).
50
+ If that phone is not a contact yet, it is created in the workspace of your key; `message.contact_id` is its id. The answer is the queued message: accepted is not delivered yet. With an official number, free text only goes within 24 hours of the contact's last message, so the first message to a new contact is an [approved template](#templates).
53
51
 
54
52
  ### 4. Know when it was read
55
53
 
@@ -71,7 +69,7 @@ app.post('/hooks/funnelchat', express.raw({ type: 'application/json' }), (req, r
71
69
  });
72
70
  ```
73
71
 
74
- The status goes `sent`, `delivered`, `read` (or `failed`, with `error`). You can also ask for it: `(await funnelchat.messages.get(contact.id, message.id)).status`. More in [Webhooks](#webhooks).
72
+ The status goes `sent`, `delivered`, `read` (or `failed`, with `error`). You can also ask for it: `funnelchat.messages.get(contactId, messageId)`, with `message.contact_id` and `message.id`. More in [Webhooks](#webhooks).
75
73
 
76
74
  ## Receive messages
77
75
 
@@ -90,10 +88,10 @@ if (event.type === 'message.received') {
90
88
 
91
89
  ## Groups
92
90
 
93
- List the groups of your numbers and send a message to one by its `uid`:
91
+ Start from the groups your number is in, read live from WhatsApp, and write to one by its `uid`:
94
92
 
95
93
  ```ts
96
- for await (const group of funnelchat.groups.list()) {
94
+ for await (const group of funnelchat.groups.listByDevice(device.id)) {
97
95
  console.log(group.uid, group.name); // '120363000000000001@g.us', 'Customers'
98
96
  }
99
97
 
@@ -104,9 +102,18 @@ const { action_id } = await funnelchat.groups.sendMessage('120363000000000001@g.
104
102
  });
105
103
  ```
106
104
 
105
+ To learn who joins and leaves a group, follow it once:
106
+
107
+ ```ts
108
+ const { data: followed } = await funnelchat.groups.follow('120363000000000001@g.us', { device_id: device.id });
109
+ console.log(followed.imported, followed.is_admin); // true, true
110
+ ```
111
+
112
+ - The live list is reused for a minute per number, so a group just created in WhatsApp can take that long to show. A group already in your account brings `device_id`; `funnelchat.groups.list()` lists those.
113
+ - Writing to a group that is not in your account follows it first, with the same rules: a number that is not admin of the group answers `422 device_not_group_admin` and nothing is imported. Following needs **Groups and campaigns → Manage groups** (`403 import_permission_required`), takes up to 5 groups per number per minute (`429 import_rate_limited`, with `retryAfter`) and works with QR numbers only (`422 provider_unsupported` with an official one). `followed.imported` is `false` when the group was already in your account.
107
114
  - The message goes out in the background: `group_action.completed` (with `action.uuid` equal to `action_id`) or `funnelchat.groups.retrieveAction(action_id)` tell you how it went. A file goes as `type: 'image'` (or `video`, `audio`, `document`) with its `https` `media_url`.
108
- - Who joins or leaves arrives by `group_participant.joined` and `group_participant.left`: `group`, `participant` (`identity`, `identity_type`, `name`) and `campaign`, `null` for a group without a campaign.
109
- - Permissions: **Groups and campaigns → View** to read and **Groups and campaigns → Send** to send.
115
+ - Who joins or leaves a followed group arrives by `group_participant.joined` and `group_participant.left`: `group`, `participant` (`identity`, `identity_type`, `name`) and `campaign`, `null` for a group without a campaign.
116
+ - Permissions: **Groups and campaigns → View** to read, **Groups and campaigns → Send** to send and **Groups and campaigns → Manage groups** to follow.
110
117
 
111
118
  ## Workspaces
112
119
 
@@ -148,7 +155,7 @@ CommonJS: `const { Funnelchat } = require('@funnelchat-hq/sdk');`. Every request
148
155
  | `funnelchat.contacts` | `list`, `create`, `upsert`, `retrieve`, `update`, `delete`, the tags and fields of a contact, `metaWindow` (the 24-hour window), notes. |
149
156
  | `funnelchat.messages` | `send`, `sendDirect`, `sendForm`, `sendContactCard`, `forward`, `get` (one message by id), `list` (the history of a contact), `react`, `removeReaction`. |
150
157
  | `funnelchat.templates` | `list`, `create`, `retrieve`, `update`, `quickReplyButtons`, `send`, `delete`. |
151
- | `funnelchat.groups` | `list`, `retrieve`, `listByDevice`, participants and their joins and leaves, messages, `sendMessage` and `retrieveAction`. |
158
+ | `funnelchat.groups` | `listByDevice` (a number's live groups), `follow`, `list`, `retrieve`, participants and their joins and leaves, messages, `sendMessage` and `retrieveAction`. |
152
159
  | `funnelchat.webhooks` | Subscriptions (`list`, `create`, `retrieve`, `update`, `delete`, `test`, `listDeliveries`), the catalog (`events`), `verify` and `constructEvent`. |
153
160
  | `funnelchat.conversations` | `list`, `update`, counters, mark everything as read or unread. |
154
161
  | `funnelchat.forms` | WhatsApp forms: `list`, `templates`, `create`, `retrieve`, `update`, `delete`, `createVersion`, `sync`, `publish`, numbers, `versions`, answers and `metrics`. |
@@ -168,7 +175,8 @@ A key has the permissions ticked when it was created. The panel groups them by a
168
175
  | Create contacts | **Contacts and conversations → Edit** |
169
176
  | Send messages and templates | **Contacts and conversations → Send** |
170
177
  | Read groups | **Groups and campaigns → View** |
171
- | Send to a group | **Groups and campaigns → Send** |
178
+ | Send to a group | **Groups and campaigns → Send** (and **Groups and campaigns → Manage groups** for one not in the account yet) |
179
+ | Follow a group | **Groups and campaigns → Manage groups** |
172
180
  | Webhook subscriptions | **Notifications (webhooks) → View** to read, **Notifications (webhooks) → Manage** for the rest, plus **View** of the area of each event |
173
181
 
174
182
  ### Send a message
@@ -191,10 +199,10 @@ if (window.is_open) {
191
199
  }
192
200
  ```
193
201
 
194
- - `contacts.create({ phone })` (7 to 15 digits) answers the existing contact when that phone is already there. A contact has to exist before you send it anything.
202
+ - **By phone, without `contact_id`:** `sendDirect` and `templates.send` take `phone` (7 to 15 digits, with country code) and create the contact in the workspace of your key when it does not exist yet, as `contacts.upsert` would; the contact stays even if a rule then stops the send. With an official number you can send `whatsapp_id` (Meta's id of the person, such as `US.13491208655302741918`) instead of `phone`; with a QR number that is a `422`. A phone that is a contact of another workspace of your account answers `404`, and nothing is created or sent. With `contact_id` too, `phone` or `whatsapp_id` must be that contact's: otherwise `422 recipient_mismatch`. Creating the contact takes **Contacts and conversations → Edit**, which **Send** includes.
195
203
  - `message.id` is the same `message_id` of `message.status_changed`. With it, `funnelchat.messages.get(2001, message.id)` tells you how it is going: `status` moves from `queued` to `sent`, `delivered`, `read` or `failed`, and a `failed` one carries `error` (`code`, `title` and `reason`).
196
204
  - `client_reference` (1 to 64 visible ASCII characters, no spaces) comes back in the history of the contact and in `message.status_changed`.
197
- - `send` is `POST /api/v1/contacts/{contact}/message` and checks the `phone` (or `whatsapp_id`) of the body against the contact of the route: when it does not match, `422 recipient_mismatch`. `sendDirect` is `POST /api/v1/message/send-message`, with `device_id` and `contact_id` in the body.
205
+ - `send` is `POST /api/v1/contacts/{contact}/message` and checks the `phone` (or `whatsapp_id`) of the body against the contact of the route: when it does not match, `422 recipient_mismatch`. `sendDirect` is `POST /api/v1/message/send-message`, with `device_id` and the recipient (`phone`, `whatsapp_id` or `contact_id`) in the body.
198
206
  - Contact cards (`sendContactCard`) return `{ messages }`, one per card; a forward (`forward`) one item per destination with its `message` (or `null` when that destination was blocked); a template (`templates.send`) and a form (`sendForm`) `{ message }`.
199
207
 
200
208
  **A file by URL.** `file` is the URL of the file. When it does not end in a known extension (a signed URL, or one without extension), say what it is with `file_type` and, for a document, the name it arrives with:
@@ -217,8 +225,7 @@ Without `file_type`, a URL without a known extension answers `422` with `errors.
217
225
 
218
226
  ```ts
219
227
  const { message } = await funnelchat.templates.send(3, '45', {
220
- phone: '5491100000000',
221
- contact_id: 2001,
228
+ phone: '5491100000000', // a new phone becomes a contact, as with sendDirect
222
229
  template_variables: { body: ['Lucía'] }, // the body variables, in order
223
230
  });
224
231
  ```
@@ -527,12 +534,11 @@ const funnelchat = new Funnelchat({ apiKey: '1|fc_live_sandbox', baseUrl: 'http:
527
534
 
528
535
  ### Pending of the spec
529
536
 
530
- The types come from the full unified spec of the developer portal (`spec/funnelchat.json`; where it came from, in `spec/procedencia.json`: developers-portal c90565e4 of 04-11-2026, with the areas at accounts v1.5.97, conversations v1.19.7 and communities v2.35.5). What production has and that spec does not describe, or describes otherwise than production answers, lives in a separate layer, `spec/pending.json`, which generation applies **only while the spec does not bring it**, or still says exactly what the entry corrects; each entry cites the production code it was read in:
537
+ The types come from the full unified spec of the developer portal (`spec/funnelchat.json`; where it came from, in `spec/procedencia.json`: developers-portal 99928866 of 05-11-2026, with the areas at accounts v1.5.97, conversations v1.19.21 and communities v2.35.7). What production has and that spec does not describe, or describes otherwise than production answers, lives in a separate layer, `spec/pending.json`, which generation applies **only while the spec does not bring it**, or still says exactly what the entry corrects; each entry cites the production code it was read in:
531
538
 
532
- - a message to a group and its action (`groups.sendMessage`, `groups.retrieveAction`, `group_action.completed`): the spec was built with the group actions off;
533
- - the events of the account (`device.*`, billing, subscriptions, monthly active contacts) and some of groups (`group.message_received`, `group.reaction_added`, `group.full`, `group_action.failed`, the join requests): the API publishes them in its catalog (`webhooks.events()`), not in the spec;
539
+ - the events of the account (`device.*`, billing, subscriptions, monthly active contacts) and some of groups (`group.message_received`, `group.reaction_added`, `group.full`, `group_action.completed` and `.failed`, the join requests): the API publishes them in its catalog (`webhooks.events()`), not in the spec;
534
540
  - WhatsApp forms (except sending one) and `webhooks.rotateSecret`;
535
- - where the spec contradicts production: what a send answers (`{ message }`, never `null`), the body of `devices.qr`, the items of `templates.list`, the answer of `webhooks.update`, and the events the spec only shows by one example (`message.received`, `message.status_changed`, `whatsapp_form.completed`, `group_participant.joined` and `.left`).
541
+ - where the spec contradicts production: what a send answers (`{ message }`, never `null`), the body of `devices.qr`, the items of `templates.list`, the answer of `webhooks.update` and of `groups.retrieveAction`, and the events the spec only shows by one example (`message.received`, `message.status_changed`, `whatsapp_form.completed`, `group_participant.joined` and `.left`).
536
542
 
537
543
  `src/pending/` has what is not OpenAPI (the `workspace_id` of every event) and `src/core/error-codes.ts` the error codes.
538
544
 
package/dist/index.cjs CHANGED
@@ -388,7 +388,13 @@ var GROUP_ACTION_ERROR_CODES = {
388
388
  /** 422: that `type` cannot be sent to this group by this number. */
389
389
  unsupported_type: 422,
390
390
  /** 429: the number has 50 group actions open. Wait for some to finish; not worth retrying. */
391
- too_many_pending_actions: 429
391
+ too_many_pending_actions: 429,
392
+ /** 403: following a group (also by writing to one not in the account) needs **Groups and campaigns → Manage groups**. */
393
+ import_permission_required: 403,
394
+ /** 429, with `Retry-After`: the number already followed 5 groups this minute. */
395
+ import_rate_limited: 429,
396
+ /** 409: another call is following the same group right now. */
397
+ import_in_progress: 409
392
398
  };
393
399
  var IDEMPOTENCY_ERROR_CODES = {
394
400
  /** 422: the key was already used with another body. Use a new key for a new resource. */
@@ -594,14 +600,29 @@ var routes = [
594
600
  { "method": "GET", "path": "/api/v1/forms/families/{familyId}/versions", "operationId": "pendingForms.versions", "domain": "conversations", "permission": "conversations:read", "tag": "WhatsappForm", "summary": "The versions of a form", "pending": true },
595
601
  { "method": "GET", "path": "/api/v1/forms/templates", "operationId": "pendingForms.templates", "domain": "conversations", "permission": "conversations:read", "tag": "WhatsappForm", "summary": "Templates to start a form", "pending": true },
596
602
  { "method": "GET", "path": "/api/v1/groups", "operationId": "group.index", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Listar grupos", "pending": false },
603
+ { "method": "POST", "path": "/api/v1/groups", "operationId": "apiGroupAction.create", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Crea un grupo", "pending": false },
597
604
  { "method": "GET", "path": "/api/v1/groups/{group}", "operationId": "group.show", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Ver un grupo", "pending": false },
598
605
  { "method": "GET", "path": "/api/v1/groups/{group}/actions", "operationId": "group.actions", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Listar las acciones de sesi\xF3n sobre un grupo", "pending": false },
606
+ { "method": "POST", "path": "/api/v1/groups/{group}/admins", "operationId": "apiGroupAction.promoteAdmins", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Promueve tel\xE9fonos a administrador", "pending": false },
607
+ { "method": "POST", "path": "/api/v1/groups/{group}/admins/remove", "operationId": "apiGroupAction.demoteAdmins", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Quita el rol de administrador a tel\xE9fonos", "pending": false },
608
+ { "method": "PUT", "path": "/api/v1/groups/{group}/description", "operationId": "apiGroupAction.updateDescription", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Cambia la descripci\xF3n del grupo", "pending": false },
609
+ { "method": "POST", "path": "/api/v1/groups/{group}/import", "operationId": "v2.apiGroupImport", "domain": "communities", "permission": "communities:operate", "tag": "Grupos", "summary": "Importar un grupo de un n\xFAmero", "pending": false },
610
+ { "method": "GET", "path": "/api/v1/groups/{group}/invite-link/live", "operationId": "apiGroupAction.inviteLink", "domain": "communities", "permission": "communities:read", "tag": "Acciones de grupo", "summary": "Link de invitaci\xF3n vigente del grupo", "pending": false },
611
+ { "method": "POST", "path": "/api/v1/groups/{group}/invite-link/renew", "operationId": "apiGroupAction.renewInviteLink", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Renueva el link de invitaci\xF3n del grupo", "pending": false },
612
+ { "method": "GET", "path": "/api/v1/groups/{group}/join-requests", "operationId": "apiGroupAction.joinRequests", "domain": "communities", "permission": "communities:read", "tag": "Acciones de grupo", "summary": "Solicitudes pendientes para unirse al grupo", "pending": false },
613
+ { "method": "POST", "path": "/api/v1/groups/{group}/join-requests/approve", "operationId": "apiGroupAction.approveJoinRequests", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Aprueba solicitudes para unirse al grupo", "pending": false },
614
+ { "method": "POST", "path": "/api/v1/groups/{group}/join-requests/reject", "operationId": "apiGroupAction.rejectJoinRequests", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Rechaza solicitudes para unirse al grupo", "pending": false },
599
615
  { "method": "GET", "path": "/api/v1/groups/{group}/messages", "operationId": "group.messages", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Listar los mensajes enviados a un grupo", "pending": false },
600
- { "method": "POST", "path": "/api/v1/groups/{group}/messages", "operationId": "pendingGroups.sendMessage", "domain": "communities", "permission": "communities:send", "tag": "GroupAction", "summary": "Send a message to a group", "pending": true },
616
+ { "method": "POST", "path": "/api/v1/groups/{group}/messages", "operationId": "apiGroupAction.sendMessage", "domain": "communities", "permission": "communities:send", "tag": "Acciones de grupo", "summary": "Mandar un mensaje a un grupo", "pending": false },
617
+ { "method": "PUT", "path": "/api/v1/groups/{group}/name", "operationId": "apiGroupAction.updateName", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Cambia el nombre del grupo", "pending": false },
601
618
  { "method": "GET", "path": "/api/v1/groups/{group}/participant-events", "operationId": "group.participantEvents", "domain": "communities", "permission": "communities:read", "tag": "Participantes", "summary": "Listar entradas y salidas de un grupo", "pending": false },
619
+ { "method": "POST", "path": "/api/v1/groups/{group}/participants", "operationId": "apiGroupAction.addParticipants", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Suma participantes al grupo", "pending": false },
620
+ { "method": "POST", "path": "/api/v1/groups/{group}/participants/remove", "operationId": "apiGroupAction.removeParticipants", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Saca participantes del grupo", "pending": false },
621
+ { "method": "PUT", "path": "/api/v1/groups/{group}/photo", "operationId": "apiGroupAction.updatePhoto", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Cambia la foto del grupo", "pending": false },
602
622
  { "method": "GET", "path": "/api/v1/groups/{group}/scheduled-messages", "operationId": "group.scheduledMessages", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Listar los env\xEDos programados que incluyen un grupo", "pending": false },
623
+ { "method": "PUT", "path": "/api/v1/groups/{group}/settings", "operationId": "apiGroupAction.updateSettings", "domain": "communities", "permission": "communities:operate", "tag": "Acciones de grupo", "summary": "Cambia si los miembros pueden escribir", "pending": false },
603
624
  { "method": "GET", "path": "/api/v1/groups/{groupUid}/metadata", "operationId": "groupMetadata.show", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Ver los datos actuales de un grupo en WhatsApp", "pending": false },
604
- { "method": "GET", "path": "/api/v1/groups/actions/{action}", "operationId": "pendingGroups.retrieveAction", "domain": "communities", "permission": "communities:read", "tag": "GroupAction", "summary": "A group action", "pending": true },
625
+ { "method": "GET", "path": "/api/v1/groups/actions/{action}", "operationId": "apiGroupAction.show", "domain": "communities", "permission": "communities:read", "tag": "Acciones de grupo", "summary": "Ver c\xF3mo sali\xF3 una acci\xF3n de grupo", "pending": true },
605
626
  { "method": "GET", "path": "/api/v1/groups/participants", "operationId": "group.participants", "domain": "communities", "permission": "communities:read", "tag": "Participantes", "summary": "Listar los participantes de un grupo", "pending": false },
606
627
  { "method": "GET", "path": "/api/v1/kanban-settings", "operationId": "kanbanSetting.index", "domain": "conversations", "permission": "conversations:read", "tag": "KanbanSetting", "summary": "Listar tableros", "pending": false },
607
628
  { "method": "POST", "path": "/api/v1/kanban-settings", "operationId": "kanbanSetting.store", "domain": "conversations", "permission": "conversations:write", "tag": "KanbanSetting", "summary": "Crear un tablero", "pending": false },
@@ -1118,7 +1139,11 @@ var Groups = class extends Resource {
1118
1139
  retrieve(group, options) {
1119
1140
  return this.client.request("GET", "/api/v1/groups/{group}", { ...options, path: { group } });
1120
1141
  }
1121
- /** `GET /api/v1/devices/{device}/groups`, paginated (`page`, `pageSize`): the groups of one number. */
1142
+ /**
1143
+ * `GET /api/v1/devices/{device}/groups`, paginated (`page`, `pageSize`): the groups of one number,
1144
+ * read live from WhatsApp (for a key, reused for a minute per number). A group that is already in
1145
+ * the account brings `device_id`; any other can be followed with {@link Groups.follow}.
1146
+ */
1122
1147
  listByDevice(device, query, options) {
1123
1148
  return this.client.paginate("/api/v1/devices/{device}/groups", { ...options, path: { device }, query });
1124
1149
  }
@@ -1161,6 +1186,20 @@ var Groups = class extends Resource {
1161
1186
  sendMessage(group, body, options) {
1162
1187
  return this.client.request("POST", "/api/v1/groups/{group}/messages", { ...options, path: { group }, body });
1163
1188
  }
1189
+ /**
1190
+ * `POST /api/v1/groups/{group}/import`: follows a WhatsApp group of a QR number (`device_id`), as
1191
+ * importing it in the panel does: from then on its joins and leaves arrive by
1192
+ * `group_participant.joined` and `group_participant.left`. Resolves to `{ data: { uid, name,
1193
+ * imported, is_admin } }`: `imported` is `false` when the group was already in the account, and
1194
+ * `is_admin` says whether the number administers it (without that it cannot write to it). Needs
1195
+ * **Groups and campaigns → Manage groups** (`403 import_permission_required`); up to 5 groups
1196
+ * per number per minute (`429 import_rate_limited`, with `retryAfter`); an official number is
1197
+ * `422 provider_unsupported`. {@link Groups.sendMessage} follows the group itself when it is not in
1198
+ * the account yet.
1199
+ */
1200
+ follow(group, body, options) {
1201
+ return this.client.request("POST", "/api/v1/groups/{group}/import", { ...options, path: { group }, body });
1202
+ }
1164
1203
  /**
1165
1204
  * `GET /api/v1/groups/actions/{action}`: where a group action, such as a message sent with
1166
1205
  * {@link Groups.sendMessage}, is (`queued`, `running`, `completed` or `failed`) and its `result`.
@@ -1448,7 +1487,7 @@ var Webhooks = class extends Resource {
1448
1487
  };
1449
1488
 
1450
1489
  // src/version.ts
1451
- var VERSION = "0.3.0";
1490
+ var VERSION = "0.3.2";
1452
1491
 
1453
1492
  // src/client.ts
1454
1493
  var BASE_URLS = {