@funnelchat-hq/sdk 0.1.1 → 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 +105 -4
- package/dist/index.cjs +161 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +670 -85
- package/dist/index.d.ts +670 -85
- package/dist/index.js +159 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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`)
|
|
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
|
|
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":
|
|
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":
|
|
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
|
-
|
|
811
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
1457
|
-
{ "type": "device.
|
|
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;
|