@funnelchat-hq/sdk 0.1.2 → 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/README.md CHANGED
@@ -56,7 +56,8 @@ Every request sends `Authorization: Bearer <key>`, `Accept: application/json` an
56
56
  | `funnelchat.tags` | `list`, `create`, `retrieve`, `update`, `delete`, `listContacts`. |
57
57
  | `funnelchat.customFields` | `list`, `create`, `retrieve`, `update`, `delete`. |
58
58
  | `funnelchat.groups` | Read groups, participants, events and messages. |
59
- | `funnelchat.devices` | `list`, `retrieve` (with `capabilities`), `update`, `status`, `health`, `me`, `checkPhone`, connection history. |
59
+ | `funnelchat.devices` | `list` (filter by `external_reference`), `create` (a QR number), `qr`, `retrieve` (with `capabilities`), `update`, `delete`, `status`, `health`, `me`, `checkPhone`, connection history. |
60
+ | `funnelchat.connectLinks` | `create`, `get`, `list`, `revoke`: a page where your customer connects a number. See [Connect a number](#connect-a-number). |
60
61
  | `funnelchat.webhooks` | Subscriptions (`list`, `create`, `retrieve`, `update`, `delete`, `test`, `listDeliveries`), the catalog (`events`), `verify` and `constructEvent`. |
61
62
 
62
63
  Parameters, bodies and responses are typed from the spec. For any other route, see [Any route](#any-route).
@@ -145,6 +146,105 @@ const { message } = await funnelchat.messages.sendForm(2001, {
145
146
 
146
147
  The answers arrive by the `whatsapp_form.completed` webhook (`answers`: `key`, `label` and `value`) and are read with `forms.listResponses(familyId)`. `forms.templates()` lists the templates to start from; `forms.update(id, { definition, expected_updated_at })` saves a draft and answers `409 STALE` when someone saved it first.
147
148
 
149
+ ## Connect a number
150
+
151
+ > **In production, except the official sign-up by link.** Creating QR numbers and connect links by QR work in production. Connect links with `type: 'meta'` are not available in production yet: they answer `503 meta_connect_disabled` until the Funnelchat domain is registered with Meta. The published spec does not describe these routes yet; see [Pending of the spec](#pending-of-the-spec).
152
+
153
+ A number belongs to the account of the key and takes a slot of its plan, as in the panel. Creating numbers, showing their QR code and creating or revoking connect links needs the `accounts:operate` permission; reading links, `accounts:read`. Tag each number with your own `external_reference` (for example the id of your customer): it comes back in the number, in the links and in every `device.*` webhook.
154
+
155
+ **A connect link** is a single-use page on the Funnelchat domain. Send your customer there: the number is created when they finish.
156
+
157
+ ```ts
158
+ const { data: link } = await funnelchat.connectLinks.create(
159
+ {
160
+ type: 'qr', // a number by QR code; or 'meta' (not in production yet)
161
+ country: 'br', // the country of the phone that will scan the QR
162
+ external_reference: 'customer-81',
163
+ return_url: 'https://your-app.example/whatsapp/done', // optional, https only
164
+ expires_in_minutes: 1440, // 30 to 10080; 24 hours by default
165
+ },
166
+ { idempotencyKey: 'connect-customer-81' },
167
+ );
168
+ console.log(link.url); // https://api.funnelchat.com/connect/… — it carries the token and is shown only now
169
+ ```
170
+
171
+ - `country` (only `qr`, two letters, optional) is the country of the phone that will scan the QR: the WhatsApp session is opened from there, so send it when you know it. It does not go with `device_id` (a reconnection creates nothing), and the link returns it, lowercase.
172
+ - `type: 'meta'` (an official WhatsApp Cloud number, **not in production yet**: `503 meta_connect_disabled`) takes `allowed_connection_types: ['coexistence', 'dedicated']`, one or both; with both, your customer chooses on the page.
173
+ - `provision_us_number: true` (only `meta`) also offers a US number of the pool. A pool number is always dedicated: it needs `dedicated` in `allowed_connection_types` and does not go with `device_id`; otherwise the API answers `422 invalid_request` (the SDK does not check it for you).
174
+ - `device_id` reconnects a number you already have, without taking a new slot; `same_number_as` (only `qr`) adds QR to a number in coexistence.
175
+ - A Meta link tolerates retries: after a failure of Facebook or Meta it stays `pending`, so your customer can try again on the page; the fifth failed attempt turns it `failed` with `facebook_auth_failed` or `meta_error`. `quota_exceeded`, `trial_number_blocked` and `number_not_owned` close it at the first attempt.
176
+ - You learn the result three ways: the `device.connected` webhook (with `connect_link_id` and `external_reference`), the `return_url` (`?connect_link_id=…&status=completed|failed&device_id=…&error_code=…`), or reading the link:
177
+
178
+ ```ts
179
+ const { data } = await funnelchat.connectLinks.get(link.id);
180
+ console.log(data.status, data.device_id, data.error_code); // 'completed', 9, null
181
+
182
+ for await (const open of funnelchat.connectLinks.list({ status: 'pending', external_reference: 'customer-81' })) {
183
+ await funnelchat.connectLinks.revoke(open.id); // only a pending link; any other answers 409
184
+ }
185
+ ```
186
+
187
+ **A QR number, without a page.** Create it and show its QR yourself:
188
+
189
+ ```ts
190
+ const { data: device } = await funnelchat.devices.create(
191
+ { connection: 'qr', alias: 'Sales', country: 'br', external_reference: 'customer-81' },
192
+ { idempotencyKey: 'qr-customer-81' },
193
+ );
194
+
195
+ const { data: qr } = await funnelchat.devices.qr(device.id);
196
+ // qr.qr_code is a PNG data URI (null once connected); ask again after qr.expires_in seconds
197
+ // (5 with a QR, 3 while the session starts, 0 once connected).
198
+
199
+ for await (const number of funnelchat.devices.list({ external_reference: 'customer-81' })) {
200
+ console.log(number.id, number.connection, number.connected);
201
+ }
202
+ ```
203
+
204
+ - **Ask for the QR every 5 seconds and have it scanned right away.** WhatsApp changes the code every few seconds and an old one no longer links ("can't link the device now"): replace the image with the latest one on each call, and never store, cache or forward a QR.
205
+ - `country` (two letters, optional) is the country of the phone that will scan the QR: the WhatsApp session is opened from there.
206
+ - An account has at most one QR number waiting to be scanned: creating another answers `409 pending_number_exists`, with that number's `device_id` in `error.body`. Show its QR instead, or delete it.
207
+ - `external_reference` changes with `devices.update(id, { external_reference })`.
208
+ - `devices.delete(id)` deletes a number and frees its slot (`accounts:operate`, irreversible). An official number stops working through Funnelchat but stays registered with Meta; a QR one logs out. It sends `device.deleted`.
209
+ - `devices.list(query, options)` takes the query first, like every list. The signature of 0.1.2, `devices.list(options)`, still works.
210
+
211
+ **`idempotencyKey`** (both creates, 1 to 255 characters) is sent as `Idempotency-Key` and kept 24 hours. The same key with the same body answers the same response without creating another number or link, so after a timeout or a `5xx` you can repeat the call with it. Only a success is replayed, with the `Idempotent-Replayed: true` header (see it with `.withResponse()`); a rejection is evaluated again. On their own, creates follow the rule of writes (see [Retries](#retries)): they are retried only on a `429` or `503`, which guarantee nothing was created.
212
+
213
+ **The webhooks of numbers:**
214
+
215
+ | Event | `data` |
216
+ |---|---|
217
+ | `device.connected` | `device`, `connected_at`, `connect_link_id`. |
218
+ | `device.disconnected` | `device`, `disconnected_at`, `connect_link_id`, `reason`: `qr_logged_out`, `meta_account_not_registered`, `shadow_ban_protection`, `provider_error`, `disconnected_by_user` or `unknown`. |
219
+ | `device.deleted` | `device`, `deleted_at`, `connect_link_id`. |
220
+ | `device.health_changed` | `device`, `previous_state`, `state` (`available`, `limited`, `blocked`, `unknown`), `changed_at`, `connect_link_id`. Official numbers, only when the state changes. |
221
+
222
+ - `device` is `{ id, name, number, connection, external_reference }`, where `connection` is `qr`, `coexistence`, `dedicated` or `null`.
223
+ - `connect_link_id`: in `device.connected`, the connect link of that connection; in the other three, the link that last connected the number. `null` when it did not come from a link.
224
+ - What these events add to the ones production sends today (`connection`, `external_reference`, `connect_link_id` and `reason`) is optional in the types: it is missing until the API that serves it is deployed. Handle `undefined`.
225
+
226
+ **When it is refused** (`NUMBER_ERROR_CODES` and `IDEMPOTENCY_ERROR_CODES`):
227
+
228
+ | Error | What happened |
229
+ |---|---|
230
+ | `403 numbers_not_included` | The plan does not include numbers. |
231
+ | `403 number_limit_reached` | Every number of the plan is in use (`limit`, `used`, `pending_device_ids` in the body). |
232
+ | `409 pending_number_exists` | A QR number is already waiting to be scanned; the body brings its `device_id`. |
233
+ | `422 qr_requires_coexistence` | QR can only be added to an official number in coexistence. |
234
+ | `422 method_already_present` | The number already has that method; the body brings its `device_id`. |
235
+ | `422 invalid_reference_device` | `same_number_as` or `device_id` is not a number of yours that can take it. |
236
+ | `422 connection_type_mismatch` | A link with `device_id` of a number that connects another way than `type`. |
237
+ | `429 too_many_open_links` | 20 connect links are open. Revoke some. |
238
+ | `503 meta_connect_disabled` | Official numbers cannot be connected by link yet, as in production today. QR links work. |
239
+ | `409 link_not_pending` | Revoking a link that already ended. |
240
+ | `422 idempotency_key_mismatch` | The key was used with another body. |
241
+ | `422 invalid_idempotency_key` | The key is not 1 to 255 characters. |
242
+ | `409 operation_in_progress` | The first call with that key is still running. |
243
+ | `422 provider_unsupported` | `devices.qr` on an official number. |
244
+ | `409` on `devices.qr` | The number cannot connect now: `trial_number_blocked`, `number_suspended_by_plan`, `device_inactive`, `provisioning_quota_exceeded`. |
245
+
246
+ `NUMBER_ERROR_CODES` lists the rest (`connect_not_allowed`, `payment_required`, `provider_error`…), with their status. `isNumberQuotaError(error.code)` tells you whether it was the quota of the plan (the first five). `too_many_open_links` and `meta_connect_disabled` are never retried automatically.
247
+
148
248
  ## Receive webhooks with Express
149
249
 
150
250
  Verify the signature over the **raw body**, exactly as it arrived. If you parse it and serialize it again, the signature changes: use `express.raw`, not `express.json`.
@@ -330,7 +430,8 @@ The published spec does not describe everything the API in production already ha
330
430
  - what sends answer (the queued message), `messages.get`, `client_reference`, `file_type` and `file_name`;
331
431
  - forms and `messages.sendForm`;
332
432
  - the `capabilities` of numbers;
333
- - webhook subscriptions and the events the spec does not bring (`message.status_changed` with its shape, `whatsapp_form.completed`, the group ones and the account ones).
433
+ - webhook subscriptions and the events the spec does not bring (`message.status_changed` with its shape, `whatsapp_form.completed`, the group ones and the account ones);
434
+ - creating and connecting numbers (`devices.create`, `devices.qr`, `country`, `external_reference`, `connectLinks`, `Idempotency-Key`) and the `device.*` events: in production (except `type: 'meta'` links), typed from the API of accounts until its spec is published.
334
435
 
335
436
  `src/pending/` has what is not OpenAPI (`device_id` in `message.received`) and `src/core/error-codes.ts` the error codes.
336
437
 
@@ -348,7 +449,7 @@ FUNNELCHAT_SANDBOX_URL=http://127.0.0.1:4010 npm run test:e2e # against a runn
348
449
 
349
450
  The SDK runs on Node 18 or later; the development tools (vitest 4, eslint 9) need Node 20 or later.
350
451
 
351
- To release, push a `vX.Y.Z` tag on `main` matching the `version` in `package.json`; GitHub Actions (`.github/workflows/publish.yml`) publishes it to npm with provenance.
452
+ To release, push a `vX.Y.Z` tag on `main` matching the `version` in `package.json`; GitHub Actions publishes it through npm Trusted Publishing (`.github/workflows/publish.yml`), with provenance.
352
453
 
353
454
  Everything generated comes from one script:
354
455
 
@@ -358,7 +459,7 @@ npm run generate # with the one in spec/, for
358
459
  ```
359
460
 
360
461
  1. With `--spec`, it copies the spec to `spec/funnelchat.json` and writes `spec/procedencia.json`: repository, branch, commit, date and `sha256` of the file.
361
- 2. It adds `spec/pending.json`, but only what the spec does not bring, and lists what it already brings so you can delete it from there.
462
+ 2. It adds `spec/pending.json`, but only what the spec does not bring, and lists what it already brings so you can delete it from there. An `operationPatches` entry without `while` adds body properties or `parameters` (such as a query filter) one by one, only while the operation lacks them.
362
463
  3. It writes `src/generated/openapi.ts` (with [openapi-typescript](https://openapi-ts.dev)), `src/generated/routes.ts` (every operation, with area and permission) and `src/generated/events.ts` (the event catalog and the shape of the `data` of each one).
363
464
 
364
465
  ```
package/dist/index.cjs CHANGED
@@ -319,11 +319,72 @@ var SEND_ERROR_CODES = {
319
319
  /** 422: the number cannot send forms. */
320
320
  number_not_eligible: 422
321
321
  };
322
+ var NUMBER_ERROR_CODES = {
323
+ /** 403: the plan of the account does not include numbers. */
324
+ numbers_not_included: 403,
325
+ /** 403: the account already uses every number of its plan (`limit`, `used`, `pending_device_ids` in the body). */
326
+ number_limit_reached: 403,
327
+ /** 403: the account cannot connect numbers now. */
328
+ connect_not_allowed: 403,
329
+ /** 403: `devices.qr` of a number whose payment is due. */
330
+ payment_required: 403,
331
+ /**
332
+ * 409: the account already has a QR number waiting to be scanned (at most one): its `device_id`
333
+ * comes in the body. Show that one's QR, or delete it, instead of creating another.
334
+ */
335
+ pending_number_exists: 409,
336
+ /** 422 (409 on `devices.qr`): QR can only be added to an official number that is in coexistence. */
337
+ qr_requires_coexistence: 422,
338
+ /** 422: the number already has that connection method; the body brings its `device_id`. */
339
+ method_already_present: 422,
340
+ /** 422: `same_number_as` or `device_id` is not a number of the account that can take it. */
341
+ invalid_reference_device: 422,
342
+ /** 422: a connect link with `device_id` of a number that connects another way than `type`. */
343
+ connection_type_mismatch: 422,
344
+ /** 422 (409 on `devices.qr`): the number is suspended because the plan has fewer numbers than the account uses. */
345
+ number_suspended_by_plan: 422,
346
+ /** 422: the body is not valid; `errors` says which fields. */
347
+ invalid_request: 422,
348
+ /** 409: `devices.qr` of a trial number that is blocked. */
349
+ trial_number_blocked: 409,
350
+ /** 409: `devices.qr` of a number that is turned off. */
351
+ device_inactive: 409,
352
+ /** 409: `devices.qr` when no more QR sessions can be started now. */
353
+ provisioning_quota_exceeded: 409,
354
+ /** 409: revoking a connect link that is no longer `pending`. */
355
+ link_not_pending: 409,
356
+ /** 429: the account has 20 connect links open. Revoke some, or wait for them to end. Not worth retrying. */
357
+ too_many_open_links: 429,
358
+ /** 500 (502 on `devices.qr`): the WhatsApp provider failed. */
359
+ provider_error: 500,
360
+ /** 503: official numbers (`type: 'meta'`) cannot be connected by link yet. QR links work. Not worth retrying. */
361
+ meta_connect_disabled: 503,
362
+ /** 403: the connect page was opened with an API key or a session. The page is for your customer, without credentials. */
363
+ credentials_not_allowed: 403
364
+ };
365
+ var IDEMPOTENCY_ERROR_CODES = {
366
+ /** 422: the key was already used with another body. Use a new key for a new resource. */
367
+ idempotency_key_mismatch: 422,
368
+ /** 422: the key is not 1 to 255 characters. */
369
+ invalid_idempotency_key: 422,
370
+ /** 409: the first call with that key, or another operation on the number, is still running. Read the state, or repeat it later. */
371
+ operation_in_progress: 409
372
+ };
322
373
  var has = (table, code) => code !== void 0 && Object.prototype.hasOwnProperty.call(table, code);
323
374
  function isSendRuleError(code) {
324
375
  return has(SEND_ERROR_CODES, code);
325
376
  }
326
- var NOT_RETRYABLE_CODES = /* @__PURE__ */ new Set(["mac_lockout_24h", "mac_limit_reached"]);
377
+ var QUOTA_CODES = /* @__PURE__ */ new Set([
378
+ "numbers_not_included",
379
+ "number_limit_reached",
380
+ "pending_number_exists",
381
+ "qr_requires_coexistence",
382
+ "method_already_present"
383
+ ]);
384
+ function isNumberQuotaError(code) {
385
+ return code !== void 0 && QUOTA_CODES.has(code);
386
+ }
387
+ var NOT_RETRYABLE_CODES = /* @__PURE__ */ new Set(["mac_lockout_24h", "mac_limit_reached", "too_many_open_links", "meta_connect_disabled"]);
327
388
 
328
389
  // src/generated/routes.ts
329
390
  var routes = [
@@ -376,6 +437,10 @@ var routes = [
376
437
  { "method": "GET", "path": "/api/v1/campaigns/{campaign}/pipelines/{pipeline}", "operationId": "campaign.showPipeline", "domain": "communities", "permission": "communities:read", "tag": "Grupos de una campa\xF1a y operaciones", "summary": "Ver un pipeline", "pending": false },
377
438
  { "method": "GET", "path": "/api/v1/campaigns/{campaign}/webhooks", "operationId": "campaignWebhook.index", "domain": "communities", "permission": "communities:read", "tag": "Webhooks de campa\xF1a (legado)", "summary": "Listar los webhooks de una campa\xF1a", "pending": false },
378
439
  { "method": "GET", "path": "/api/v1/campaigns/{campaign}/webhooks/logs", "operationId": "campaignWebhook.logs", "domain": "communities", "permission": "communities:read", "tag": "Webhooks de campa\xF1a (legado)", "summary": "Listar las entregas de los webhooks de una campa\xF1a", "pending": false },
440
+ { "method": "GET", "path": "/api/v1/connect-links", "operationId": "pendingConnectLink.index", "domain": "accounts", "permission": "accounts:read", "tag": "ConnectLink", "summary": "List connect links", "pending": true },
441
+ { "method": "POST", "path": "/api/v1/connect-links", "operationId": "pendingConnectLink.store", "domain": "accounts", "permission": "accounts:operate", "tag": "ConnectLink", "summary": "Create a connect link", "pending": true },
442
+ { "method": "DELETE", "path": "/api/v1/connect-links/{link}", "operationId": "pendingConnectLink.destroy", "domain": "accounts", "permission": "accounts:operate", "tag": "ConnectLink", "summary": "Revoke a connect link", "pending": true },
443
+ { "method": "GET", "path": "/api/v1/connect-links/{link}", "operationId": "pendingConnectLink.show", "domain": "accounts", "permission": "accounts:read", "tag": "ConnectLink", "summary": "Read a connect link", "pending": true },
379
444
  { "method": "PUT", "path": "/api/v1/contact-flows/{contactFlowId}/cancel", "operationId": "flow.cancelContactFlow", "domain": "conversations", "permission": "conversations:write", "tag": "Flow", "summary": "Cancelar un flujo en curso", "pending": false },
380
445
  { "method": "GET", "path": "/api/v1/contact-flows/{contactId}", "operationId": "flow.contactFlowHistory", "domain": "conversations", "permission": "conversations:read", "tag": "Flow", "summary": "Listar los flujos en curso de un contacto", "pending": false },
381
446
  { "method": "GET", "path": "/api/v1/contacts", "operationId": "contact.index", "domain": "conversations", "permission": "conversations:read", "tag": "Contact", "summary": "Listar contactos", "pending": false },
@@ -425,10 +490,11 @@ var routes = [
425
490
  { "method": "GET", "path": "/api/v1/custom-fields/{customField}", "operationId": "customField.show", "domain": "conversations", "permission": "conversations:read", "tag": "CustomField", "summary": "Ver un campo personalizado", "pending": false },
426
491
  { "method": "PUT", "path": "/api/v1/custom-fields/{customField}", "operationId": "customField.update", "domain": "conversations", "permission": "conversations:write", "tag": "CustomField", "summary": "Actualizar un campo personalizado", "pending": false },
427
492
  { "method": "POST", "path": "/api/v1/custom-fields/delete", "operationId": "customField.destroy", "domain": "conversations", "permission": "conversations:write", "tag": "CustomField", "summary": "Eliminar campos personalizados", "pending": false },
428
- { "method": "GET", "path": "/api/v1/devices", "operationId": "device.index", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Listar n\xFAmeros", "pending": false },
493
+ { "method": "GET", "path": "/api/v1/devices", "operationId": "device.index", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Listar n\xFAmeros", "pending": true },
494
+ { "method": "POST", "path": "/api/v1/devices", "operationId": "pendingDevice.store", "domain": "accounts", "permission": "accounts:operate", "tag": "Device", "summary": "Create a number connected by QR code", "pending": true },
429
495
  { "method": "DELETE", "path": "/api/v1/devices/{device}", "operationId": "device.destroy", "domain": "accounts", "permission": "accounts:operate", "tag": "Device", "summary": "Eliminar un n\xFAmero", "pending": false },
430
496
  { "method": "GET", "path": "/api/v1/devices/{device}", "operationId": "device.show", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Ver un n\xFAmero", "pending": false },
431
- { "method": "PUT", "path": "/api/v1/devices/{device}", "operationId": "device.update", "domain": "accounts", "permission": "accounts:write", "tag": "Device", "summary": "Actualizar un n\xFAmero", "pending": false },
497
+ { "method": "PUT", "path": "/api/v1/devices/{device}", "operationId": "device.update", "domain": "accounts", "permission": "accounts:write", "tag": "Device", "summary": "Actualizar un n\xFAmero", "pending": true },
432
498
  { "method": "GET", "path": "/api/v1/devices/{device}/ad-groups", "operationId": "group.adGroups", "domain": "communities", "permission": "communities:read", "tag": "Grupos", "summary": "Listar los grupos de anuncios de un n\xFAmero", "pending": false },
433
499
  { "method": "POST", "path": "/api/v1/devices/{device}/capi/dataset", "operationId": "capiConnection.dataset", "domain": "accounts", "permission": "accounts:write", "tag": "CapiConnection", "summary": "Configurar el dataset de Conversions API", "pending": false },
434
500
  { "method": "GET", "path": "/api/v1/devices/{device}/capi/test", "operationId": "capiConnection.test", "domain": "accounts", "permission": "accounts:read", "tag": "CapiConnection", "summary": "Probar la conexi\xF3n con Conversions API", "pending": false },
@@ -452,6 +518,7 @@ var routes = [
452
518
  { "method": "DELETE", "path": "/api/v1/devices/{device}/message-queue/all", "operationId": "device.clearQueue", "domain": "accounts", "permission": "accounts:operate", "tag": "Device", "summary": "Vaciar la cola de mensajes", "pending": false },
453
519
  { "method": "GET", "path": "/api/v1/devices/{device}/message-queue/count", "operationId": "device.getQueueCount", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Contar los mensajes en la cola", "pending": false },
454
520
  { "method": "GET", "path": "/api/v1/devices/{device}/open-conversations", "operationId": "device.openConversations", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Ver qu\xE9 tel\xE9fonos tienen conversaci\xF3n abierta", "pending": false },
521
+ { "method": "GET", "path": "/api/v1/devices/{device}/qr", "operationId": "pendingDevice.qr", "domain": "accounts", "permission": "accounts:operate", "tag": "Device", "summary": "The QR code to connect a number", "pending": true },
455
522
  { "method": "PUT", "path": "/api/v1/devices/{device}/reset", "operationId": "device.reset", "domain": "accounts", "permission": "accounts:operate", "tag": "Device", "summary": "Reiniciar la sesi\xF3n de un n\xFAmero", "pending": false },
456
523
  { "method": "GET", "path": "/api/v1/devices/{device}/status", "operationId": "device.status", "domain": "accounts", "permission": "accounts:read", "tag": "Device", "summary": "Ver el estado de conexi\xF3n", "pending": false },
457
524
  { "method": "DELETE", "path": "/api/v1/devices/{device}/templates/{template}", "operationId": "messageTemplate.delete", "domain": "conversations", "permission": "conversations:write", "tag": "MessageTemplate", "summary": "Eliminar una plantilla de un n\xFAmero", "pending": false },
@@ -676,6 +743,42 @@ var Resource = class {
676
743
  get client() {
677
744
  return this.#client;
678
745
  }
746
+ /** The options of a request with `idempotencyKey` turned into the `Idempotency-Key` header. */
747
+ idempotent({ idempotencyKey, ...options } = {}) {
748
+ if (idempotencyKey === void 0) return options;
749
+ return { ...options, headers: { ...options.headers, "Idempotency-Key": idempotencyKey } };
750
+ }
751
+ };
752
+
753
+ // src/resources/connect-links.ts
754
+ var ConnectLinks = class extends Resource {
755
+ /**
756
+ * `POST /api/v1/connect-links`. `url` is the page to send your customer to: it carries the token
757
+ * and comes only in this response.
758
+ *
759
+ * `provision_us_number` (a US number of the pool, always dedicated) needs `dedicated` in
760
+ * `allowed_connection_types` and does not go with `device_id`: otherwise `422 invalid_request`.
761
+ * A Meta link stays `pending` after a failure of Facebook or Meta, so your customer can try again;
762
+ * the fifth failed attempt turns it `failed`.
763
+ *
764
+ * `country` (only `qr`, not with `device_id`) is the country of the phone that will scan the QR, in
765
+ * two letters: the WhatsApp session is opened from there. The link returns it, lowercase.
766
+ */
767
+ create(body, options) {
768
+ return this.client.request("POST", "/api/v1/connect-links", { ...this.idempotent(options), body });
769
+ }
770
+ /** `GET /api/v1/connect-links/{link}`: its `status`, and `device_id` and `error_code` once it ends. */
771
+ get(link, options) {
772
+ return this.client.request("GET", "/api/v1/connect-links/{link}", { ...options, path: { link } });
773
+ }
774
+ /** `GET /api/v1/connect-links`, paginated, newest first: filter by `status` or `external_reference`. */
775
+ list(query, options) {
776
+ return this.client.paginate("/api/v1/connect-links", { ...options, query });
777
+ }
778
+ /** `DELETE /api/v1/connect-links/{link}`: revokes a `pending` link (any other is `409 link_not_pending`). */
779
+ revoke(link, options) {
780
+ return this.client.request("DELETE", "/api/v1/connect-links/{link}", { ...options, path: { link } });
781
+ }
679
782
  };
680
783
 
681
784
  // src/resources/contacts.ts
@@ -806,10 +909,30 @@ var CustomFields = class extends Resource {
806
909
  };
807
910
 
808
911
  // src/resources/devices.ts
912
+ var REQUEST_OPTION_KEYS = /* @__PURE__ */ new Set(["headers", "timeoutMs", "maxRetries", "retrySend", "signal"]);
913
+ function isRequestOptions(value) {
914
+ if (typeof value !== "object" || value === null) return false;
915
+ const keys = Object.keys(value);
916
+ return keys.length > 0 && keys.every((key) => REQUEST_OPTION_KEYS.has(key));
917
+ }
809
918
  var Devices = class extends Resource {
810
- /** `GET /api/v1/devices`. Iterate it with `for await`, or `await` it for the first page. */
811
- list(options) {
812
- return this.client.paginate("/api/v1/devices", { ...options });
919
+ list(queryOrOptions, options) {
920
+ if (options === void 0 && isRequestOptions(queryOrOptions)) return this.client.paginate("/api/v1/devices", { ...queryOrOptions });
921
+ return this.client.paginate("/api/v1/devices", { ...options, query: queryOrOptions });
922
+ }
923
+ /**
924
+ * `POST /api/v1/devices`: creates a number that connects by QR code (`connection: 'qr'`), with
925
+ * `connected: false`. Then show its QR with `qr`, or send your customer a connect link
926
+ * (`connectLinks.create`). Needs `accounts:operate`. It takes a slot of the plan, with the panel's
927
+ * rejections: `403 numbers_not_included`, `403 number_limit_reached`, `422 qr_requires_coexistence`
928
+ * and `422 method_already_present` (`same_number_as` adds QR to a number in coexistence instead).
929
+ * An account has at most one QR number waiting to be scanned: another one is `409
930
+ * pending_number_exists`, with that number's `device_id` in `error.body`. `country` (two letters,
931
+ * such as `'br'`) is the country of the phone that will scan the QR: the WhatsApp session is opened
932
+ * from there.
933
+ */
934
+ create(body, options) {
935
+ return this.client.request("POST", "/api/v1/devices", { ...this.idempotent(options), body });
813
936
  }
814
937
  /**
815
938
  * `GET /api/v1/devices/{device}`. `capabilities` says what the number can do by API: message
@@ -818,10 +941,30 @@ var Devices = class extends Resource {
818
941
  retrieve(device, options) {
819
942
  return this.client.request("GET", "/api/v1/devices/{device}", { ...options, path: { device } });
820
943
  }
821
- /** `PUT /api/v1/devices/{device}`. */
944
+ /**
945
+ * `DELETE /api/v1/devices/{device}`: deletes the number and frees its slot of the plan. An
946
+ * official number stops receiving through Funnelchat but stays registered with Meta; a QR one
947
+ * logs out. Irreversible; needs `accounts:operate`. Sends `device.deleted`.
948
+ */
949
+ delete(device, options) {
950
+ return this.client.request("DELETE", "/api/v1/devices/{device}", { ...options, path: { device } });
951
+ }
952
+ /** `PUT /api/v1/devices/{device}`: also changes `external_reference`. */
822
953
  update(device, body, options) {
823
954
  return this.client.request("PUT", "/api/v1/devices/{device}", { ...options, path: { device }, body });
824
955
  }
956
+ /**
957
+ * `GET /api/v1/devices/{device}/qr`: the QR code to scan, as a PNG data URI (`qr_code`), or `null`
958
+ * once `connected`. Ask for it every `expires_in` seconds (5 with a QR, 3 while the session starts,
959
+ * 0 once connected) and have it scanned right away: WhatsApp changes the code every few seconds and
960
+ * an old one no longer links, so show the latest and never store or forward a QR. Only for QR
961
+ * numbers (an official one is `422 provider_unsupported`); needs
962
+ * `accounts:operate`, since scanning it gives control of a WhatsApp. A number that cannot connect now
963
+ * answers `409` (`trial_number_blocked`, `number_suspended_by_plan`, `device_inactive`…).
964
+ */
965
+ qr(device, options) {
966
+ return this.client.request("GET", "/api/v1/devices/{device}/qr", { ...options, path: { device } });
967
+ }
825
968
  /** `GET /api/v1/devices/{device}/status`: connection status. */
826
969
  status(device, options) {
827
970
  return this.client.request("GET", "/api/v1/devices/{device}/status", { ...options, path: { device } });
@@ -1241,7 +1384,7 @@ var Webhooks = class extends Resource {
1241
1384
  };
1242
1385
 
1243
1386
  // src/version.ts
1244
- var VERSION = "0.1.2";
1387
+ var VERSION = "0.2.0";
1245
1388
 
1246
1389
  // src/client.ts
1247
1390
  var BASE_URLS = {
@@ -1268,6 +1411,8 @@ var Funnelchat = class {
1268
1411
  customFields;
1269
1412
  groups;
1270
1413
  devices;
1414
+ /** Single-use pages where your customer connects a WhatsApp number to your account. */
1415
+ connectLinks;
1271
1416
  /** Webhook subscriptions, plus `verify` and `constructEvent`. */
1272
1417
  webhooks;
1273
1418
  // Private fields: the key never shows up in `console.log(client)` or in JSON.
@@ -1302,6 +1447,7 @@ var Funnelchat = class {
1302
1447
  this.customFields = new CustomFields(this);
1303
1448
  this.groups = new Groups(this);
1304
1449
  this.devices = new Devices(this);
1450
+ this.connectLinks = new ConnectLinks(this);
1305
1451
  this.webhooks = new Webhooks(this);
1306
1452
  }
1307
1453
  /** What `JSON.stringify(client)` shows: the configuration, never the key. */
@@ -1453,8 +1599,10 @@ var webhookEvents = [
1453
1599
  { "type": "conversation.assigned", "domains": ["conversations"], "status": "live", "pending": false, "description": "Una conversaci\xF3n qued\xF3 asignada a una persona del equipo o a un agente de IA.\n\nSe env\xEDa cuando una conversaci\xF3n queda asignada a una persona del equipo (`assignee.type` = `human`) o a un agente de IA (`assignee.type` = `ai`), por cualquier camino. `assignee` trae el tipo y el nombre, nunca un identificador. No se env\xEDa al desasignar, ni cuando se vuelve a enviar el mismo asignado." },
1454
1600
  { "type": "conversation.closed", "domains": ["conversations"], "status": "live", "pending": false, "description": "Una conversaci\xF3n pas\xF3 a cerrada.\n\nSe env\xEDa cuando una conversaci\xF3n pasa de abierta a cerrada. No se env\xEDa si la conversaci\xF3n ya estaba cerrada ni cuando se reabre." },
1455
1601
  { "type": "daily_report.generated", "domains": ["communities"], "status": "live", "pending": true, "description": "The daily report of the account was generated (clicks, joins and leaves)." },
1456
- { "type": "device.connected", "domains": ["accounts"], "status": "live", "pending": true, "description": "One of the numbers of the account got connected: by scanning the QR code or by finishing the sign-up of a WhatsApp Cloud number." },
1457
- { "type": "device.disconnected", "domains": ["accounts"], "status": "live", "pending": true, "description": "One of the numbers of the account disconnected and can no longer send or receive." },
1602
+ { "type": "device.connected", "domains": ["accounts"], "status": "live", "pending": true, "description": "One of the numbers of the account got connected: by scanning the QR code, by finishing the sign-up of a WhatsApp Cloud number, or through a connect link." },
1603
+ { "type": "device.deleted", "domains": ["accounts"], "status": "planned", "pending": true, "description": "A number of the account was deleted: its slot of the plan is free again." },
1604
+ { "type": "device.disconnected", "domains": ["accounts"], "status": "live", "pending": true, "description": "One of the numbers of the account disconnected and can no longer send or receive: a QR session that closed or was protected from a shadow ban, an official number that failed its re-registration, or one disconnected from the panel." },
1605
+ { "type": "device.health_changed", "domains": ["accounts"], "status": "planned", "pending": true, "description": "The health of an official number changed (what Meta lets it do). Emitted only when the overall state changes." },
1458
1606
  { "type": "flow.completed", "domains": ["conversations"], "status": "live", "pending": false, "description": "Un contacto termin\xF3 un flujo de automatizaci\xF3n.\n\nSe env\xEDa una sola vez por cada ejecuci\xF3n de un flujo, cuando termina con \xE9xito. No se env\xEDa si la ejecuci\xF3n se cort\xF3, se cancel\xF3, qued\xF3 bloqueada o fall\xF3. `contact` es `null` si la ejecuci\xF3n no tiene un contacto asociado. Un flujo muy usado puede generar muchos eventos seguidos." },
1459
1607
  { "type": "flow.failed", "domains": ["conversations"], "status": "live", "pending": false, "description": "La ejecuci\xF3n de un flujo para un contacto termin\xF3 por un error.\n\nSe env\xEDa una sola vez por cada ejecuci\xF3n de un flujo que termina por un error, no porque lleg\xF3 al final. `reason` indica el motivo y toma uno de estos valores: `node_missing` (el paso a ejecutar ya no existe), `unknown_node_type` (el paso es de un tipo no reconocido), `missing_subflow` (el flujo que deb\xEDa ejecutarse dentro de otro ya no existe), `malformed_node` (el paso tiene una configuraci\xF3n da\xF1ada), `loop_detected` (la ejecuci\xF3n dio vueltas sin esperar nada) y `execution_error` (un error inesperado detuvo la ejecuci\xF3n). No se env\xEDa si la ejecuci\xF3n se cancel\xF3, qued\xF3 bloqueada o se cort\xF3 porque se edit\xF3 el flujo o se unieron contactos. `contact` es `null` si la ejecuci\xF3n no tiene un contacto asociado." },
1460
1608
  { "type": "group_action.failed", "domains": ["communities"], "status": "live", "pending": true, "description": "An action on a group of a campaign failed." },
@@ -1505,7 +1653,9 @@ exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
1505
1653
  exports.DEFAULT_TOLERANCE_SECONDS = DEFAULT_TOLERANCE_SECONDS;
1506
1654
  exports.Funnelchat = Funnelchat;
1507
1655
  exports.FunnelchatError = FunnelchatError;
1656
+ exports.IDEMPOTENCY_ERROR_CODES = IDEMPOTENCY_ERROR_CODES;
1508
1657
  exports.InternalServerError = InternalServerError;
1658
+ exports.NUMBER_ERROR_CODES = NUMBER_ERROR_CODES;
1509
1659
  exports.NotFoundError = NotFoundError;
1510
1660
  exports.Page = Page;
1511
1661
  exports.PagePromise = PagePromise;
@@ -1520,6 +1670,7 @@ exports.VERSION = VERSION;
1520
1670
  exports.WebhookVerificationError = WebhookVerificationError;
1521
1671
  exports.constructEvent = constructEvent;
1522
1672
  exports.default = Funnelchat;
1673
+ exports.isNumberQuotaError = isNumberQuotaError;
1523
1674
  exports.isSendRuleError = isSendRuleError;
1524
1675
  exports.routes = routes;
1525
1676
  exports.verify = verify;