@funnelchat-hq/sandbox 0.1.1 → 0.3.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 +282 -49
- package/dist/{chunk-KH6A3I2A.js → chunk-PCGMUC35.js} +13673 -3512
- package/dist/cli.js +4 -1
- package/dist/index.d.ts +209 -7
- package/dist/index.js +1 -1
- package/funnelchat-sandbox.config.example.json +4 -0
- package/package.json +1 -1
- package/spec/procedencia.json +32 -28
package/README.md
CHANGED
|
@@ -1,27 +1,125 @@
|
|
|
1
1
|
# @funnelchat-hq/sandbox
|
|
2
2
|
|
|
3
|
-
A local server that imitates the Funnelchat public API and sends signed webhooks, with no real effects
|
|
3
|
+
A local server that imitates the Funnelchat public API and sends signed webhooks, with no real effects: nothing goes out to WhatsApp, nothing is charged, everything stays in memory. Build and test your integration without an account.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- In-memory state for a real flow: contacts, conversations and messages, tags, custom fields, templates, WhatsApp forms and webhook subscriptions.
|
|
7
|
-
- A send answers at once with the queued message, and then announces `sent`, `delivered` and `read` through `message.status_changed`.
|
|
8
|
-
- Webhooks signed with the portal's exact algorithm, with its retries, and the event catalog of production.
|
|
9
|
-
- The WhatsApp rules the API applies: the 24-hour window on official numbers, the per-number pace, the spam brakes and the MAC lockout.
|
|
10
|
-
- `X-Request-Id` on every response, like the API.
|
|
5
|
+
It imitates production on 04-11: accounts v1.5.97, conversations v1.19.7, communities v2.35.5, platform-gate v0.9.1 and the API edge v0.3.0.
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
## Try it in 3 minutes, without an account
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
You need Node 20.11 or later. Start the sandbox and leave it running:
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npx @funnelchat-hq/sandbox
|
|
13
|
+
```
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
In another terminal, the key. Against the sandbox any well-formed key works; against Funnelchat you create it in the panel (Developers → Create key) and it is shown only once. Either way, read it from `FUNNELCHAT_API_KEY`:
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
|
-
|
|
18
|
+
export FUNNELCHAT_API_KEY='1|fc_live_sandbox'
|
|
19
|
+
API=http://localhost:4010/api/v1
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**1. Connect a number.** Create a number by QR, ask for its QR and "scan" it (in the sandbox the scan is a route, or the **Scan the QR** button of the panel):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -s -X POST $API/devices -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
|
|
26
|
+
-H 'Content-Type: application/json' -d '{"connection": "qr"}'
|
|
27
|
+
# {"data": {"id": 5, "connected": 0, "connection": "qr", …}}
|
|
28
|
+
curl -s $API/devices/5/qr -H "Authorization: Bearer $FUNNELCHAT_API_KEY"
|
|
29
|
+
# {"data": {"connected": false, "qr_code": "data:image/png;base64,…", "expires_in": 5}}: show it, ask again every 5 s
|
|
30
|
+
curl -s -X POST http://localhost:4010/_sandbox/devices/5/connect
|
|
31
|
+
# device.connected: number 5 is connected
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**2. Send a message.** Create the contact first, then send it a text through number 5:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -s -X POST $API/contacts -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
|
|
38
|
+
-H 'Content-Type: application/json' -d '{"name": "Ana", "phone": "5491155550000"}'
|
|
39
|
+
# {"id": 2002, "phone": "5491155550000", …}
|
|
40
|
+
curl -s -X POST $API/contacts/2002/message -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
|
|
41
|
+
-H 'Content-Type: application/json' \
|
|
42
|
+
-d '{"device_id": 5, "contact_id": 2002, "phone": "5491155550000", "text": "Hola, Ana", "client_reference": "pedido-8841"}'
|
|
43
|
+
# {"message": {"id": 9001, "status": "queued", "client_reference": "pedido-8841", …}}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**3. Know when it was read.** Within six seconds the message goes `sent` → `delivered` → `read`. Watch it at [http://localhost:4010/_sandbox](http://localhost:4010/_sandbox), under **Events** (every `message.status_changed`, delivered or not), or read the message:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -s $API/contacts/2002/messages/9001 -H "Authorization: Bearer $FUNNELCHAT_API_KEY"
|
|
50
|
+
# {"id": 9001, "status": "read", …}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
In your code, subscribe a webhook to `message.status_changed` and verify its signature (see [Webhooks](#webhooks) and [End to end](#end-to-end)). That is the whole first use; against Funnelchat it is the same calls with your key and `https://api.funnelchat.com` instead of `http://localhost:4010`.
|
|
54
|
+
|
|
55
|
+
### Receive messages
|
|
56
|
+
|
|
57
|
+
A contact writes to one of your numbers: the sandbox creates the contact if it is new (`contact.created`) and emits `message.received`, with the `device_id` that received it. From the panel (**Incoming message**) or:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
curl -s -X POST http://localhost:4010/_sandbox/inbound/message -H 'Content-Type: application/json' \
|
|
61
|
+
-d '{"phone": "5491155550000", "device_id": 5, "text": "¿Tienen stock?"}'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
On an official number (`3`) this also opens the 24-hour window: free text only goes out within it, after that only [templates](#sending-a-message).
|
|
65
|
+
|
|
66
|
+
### Groups
|
|
67
|
+
|
|
68
|
+
The groups of your numbers, with the shape of communities: `GET /api/v1/groups` (paginated, 25 per page, sorted by `created_at`; `name`, `deviceId`… filter) and `GET /api/v1/groups/{uid}`. The sandbox starts with one, `120363000000000000@g.us`, of the QR number `4`; `POST /_sandbox/groups` (`{"device_id": 5, "name": "Clientes", "participants": ["5491155550000"]}`) gives a connected QR number another one.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -s "$API/groups" -H "Authorization: Bearer $FUNNELCHAT_API_KEY"
|
|
72
|
+
# {"data": [{"id": 7001, "uid": "120363000000000000@g.us", "name": "Grupo de ejemplo", "deviceId": 4, "participants_count": 2, …}], "links": {…}, "meta": {…}}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Someone joins or leaves a group:** `group_participant.joined` and `group_participant.left`, with `group`, `campaign` (`null` without one) and `participant` (`identity`, the phone in digits). In the sandbox they are simulated:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -s -X POST http://localhost:4010/_sandbox/groups/participant-joined -H 'Content-Type: application/json' \
|
|
79
|
+
-d '{"group_id": 7001, "participant": {"phone": "5491155550000", "name": "Ana"}}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Sending a message to a group** is `POST /api/v1/groups/{uid}/messages`, a group action: it answers `202` with an `action_id`, and `GET /api/v1/groups/actions/{action}` says when it is `completed` (with `result.message.uid`, the message's id in WhatsApp), as does the event `group_action.completed`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -s -X POST "$API/groups/120363000000000000@g.us/messages" -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
|
|
86
|
+
-H 'Content-Type: application/json' -d '{"device_id": 4, "type": "text", "text": "Hola a todos"}'
|
|
87
|
+
# 202 {"action_id": "…", "status": "queued"}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`type` is `text` (with `text`), `image`, `video`, `audio` or `document` (with `media_url`, an `https` URL with an extension of the type; `caption` for `image` and `video`, `file_name` for `document`). The number must be a QR number that is an admin of the group: `404 device_not_found` or `group_not_found`, `422 device_not_connected`, `device_not_group_admin` or, for an official number, `provider_unsupported` (the sandbox account is not in the beta of official groups). To imitate communities' switch `API_GROUP_ACTIONS_ENABLED` off, start it with `--no-group-actions`: every key then gets `403 route_not_available`.
|
|
91
|
+
|
|
92
|
+
### Workspaces
|
|
93
|
+
|
|
94
|
+
An account can have several workspaces (one per client of an agency, for example), and every key belongs to one: it only sees and operates the numbers, contacts, conversations, messages, groups and webhook subscriptions of its workspace, and what it creates lands there. A record of another workspace answers like one that does not exist (`404`). Every event carries `data.workspace_id`, the workspace it belongs to, and reaches only the subscriptions of that workspace.
|
|
95
|
+
|
|
96
|
+
The sandbox starts with one workspace, `1` («Principal»), and every well-formed key belongs to it, so nothing above changes. To try a second one, create it with its key (in production it is created in the panel, and its keys in Developers):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -s -X POST http://localhost:4010/_sandbox/workspaces -H 'Content-Type: application/json' -d '{"name": "Cliente B"}'
|
|
100
|
+
# {"workspace": {"id": 2, "name": "Cliente B", "is_default": false, "keys": ["101|fc_live_…"]}, "key": "101|fc_live_…"}
|
|
22
101
|
```
|
|
23
102
|
|
|
24
|
-
|
|
103
|
+
With that key, `GET /api/v1/devices` is empty until you connect a number there, and its events go out with `"workspace_id": 2`. `POST /_sandbox/keys` (`{"workspace_id": 2}`) gives another key, `GET /_sandbox/workspaces` lists them, and the configuration file can create workspaces with fixed keys (see [Options](#options)).
|
|
104
|
+
|
|
105
|
+
## Reference
|
|
106
|
+
|
|
107
|
+
**About the language.** This documentation, the command line, the panel and the sandbox's own routes (`/_sandbox`) are in English. The answers under `/api/v1` imitate the API, which answers in Spanish, so their messages are in Spanish, word for word where they come from the API's code. Decide by the HTTP status and by `error`, never by `message`.
|
|
108
|
+
|
|
109
|
+
What it imitates, beyond the first use:
|
|
110
|
+
|
|
111
|
+
- The same routes, bodies and errors as the API in production, validated against its spec.
|
|
112
|
+
- In-memory state for a real flow: numbers and sign-up links, contacts, conversations and messages, groups, tags, custom fields, templates, WhatsApp forms and webhook subscriptions, in one or more workspaces.
|
|
113
|
+
- A send answers at once with the queued message, and then announces `sent`, `delivered` and `read` through `message.status_changed`.
|
|
114
|
+
- Webhooks signed with the portal's exact algorithm, with its retries, and the event catalog of production.
|
|
115
|
+
- The WhatsApp rules the API applies: the 24-hour window on official numbers, the per-number pace, the spam brakes and the MAC lockout.
|
|
116
|
+
- `X-Request-Id` on every response, like the API.
|
|
117
|
+
|
|
118
|
+
What it does not imitate is in [What it does not imitate](#what-it-does-not-imitate).
|
|
119
|
+
|
|
120
|
+
## Starting it
|
|
121
|
+
|
|
122
|
+
You need Node 20.11 or later. `npx @funnelchat-hq/sandbox` starts it; or, in your project, as a development dependency (the command it installs is `funnelchat-sandbox`):
|
|
25
123
|
|
|
26
124
|
```bash
|
|
27
125
|
npm install --save-dev @funnelchat-hq/sandbox
|
|
@@ -29,7 +127,7 @@ npx funnelchat-sandbox
|
|
|
29
127
|
```
|
|
30
128
|
|
|
31
129
|
```
|
|
32
|
-
funnelchat-sandbox 0.
|
|
130
|
+
funnelchat-sandbox 0.3.0 at http://127.0.0.1:4010
|
|
33
131
|
API: http://127.0.0.1:4010/api/v1/... (Authorization: Bearer 1|fc_live_sandbox, or any well-formed key)
|
|
34
132
|
panel: http://127.0.0.1:4010/_sandbox
|
|
35
133
|
example numbers: 3 (official, with the 24-hour window) and 4 (QR, no window); contact 2001; template 45
|
|
@@ -73,6 +171,7 @@ await sandbox.close();
|
|
|
73
171
|
|---|---|---|
|
|
74
172
|
| `-p`, `--port <n>` | The port. `0` takes a free one. | `4010` |
|
|
75
173
|
| `--host <host>` | Where it listens. | `127.0.0.1` (in Docker, `0.0.0.0`) |
|
|
174
|
+
| `--public-url <url>` | The base of the sign-up links' `url` (`<url>/connect/<token>`). Set it when the sandbox is reached through another address, as in Docker. | Where it listens |
|
|
76
175
|
| `-c`, `--config <file>` | A JSON configuration file. | `./funnelchat-sandbox.config.json`, if it exists |
|
|
77
176
|
| `--status-delays <s,d,r>` | Seconds until `sent`, from there to `delivered` and from there to `read`. A `-` stops there (for example `1,2,-`: never read). | `1,2,3` |
|
|
78
177
|
| `--retry-delays <a,b,c>` | Seconds between the attempts of a webhook delivery. | `60,300,1800`, like the portal |
|
|
@@ -82,6 +181,8 @@ await sandbox.close();
|
|
|
82
181
|
| `--no-antiblock` | Do not apply the spam brakes. | — |
|
|
83
182
|
| `--no-rate-limit` | Do not apply the per-number pace. | — |
|
|
84
183
|
| `--template-approval <s>` | Seconds until a template created through the API is approved. A `-` leaves it pending. | `5` |
|
|
184
|
+
| `--meta-connect` | Turns on the [Meta sign-up by link](#connecting-numbers), off like in production. | off |
|
|
185
|
+
| `--no-group-actions` | Turns off [sending a message to a group](#groups) (`403 route_not_available`), on like in production. | on |
|
|
85
186
|
| `-q`, `--quiet` | No line per request and per delivery on the console. | — |
|
|
86
187
|
|
|
87
188
|
Options win over the file. The file has the same options and, besides, subscriptions created at start (and after every reset) with a fixed secret, so your receiver keeps working when you restart the sandbox:
|
|
@@ -101,17 +202,22 @@ Options win over the file. The file has the same options and, besides, subscript
|
|
|
101
202
|
}
|
|
102
203
|
```
|
|
103
204
|
|
|
205
|
+
The file also takes `groupActions` (`false` is `--no-group-actions`), `numbers`, the quota of the account for [connecting numbers](#connecting-numbers): `{"limit": 5, "included": true, "trial": false, "metaConnect": false, "linksPerMinute": 10}`, and `publicUrl`. `metaConnect`
|
|
206
|
+
is the Meta sign-up by link: off by default, like production; `true` (or `--meta-connect`) turns it on to test it.
|
|
207
|
+
|
|
208
|
+
And `workspaces`, more [workspaces](#workspaces) with fixed keys, created after the default one (ids `2`, `3`… in order): `[{"name": "Cliente B", "keys": ["2|fc_live_clienteB"]}]`. Any key not listed there belongs to the default workspace; the preset subscriptions too.
|
|
209
|
+
|
|
104
210
|
There is a complete one in [funnelchat-sandbox.config.example.json](funnelchat-sandbox.config.example.json). The file `funnelchat-sandbox.config.json` is in `.gitignore`: do not commit a secret, even a test one.
|
|
105
211
|
|
|
106
212
|
## The key
|
|
107
213
|
|
|
108
|
-
Any key with the shape of a real one works: `Authorization: Bearer <id>|fc_live_<letters and digits>`, for example `1|fc_live_sandbox`. Without a key, or with a malformed one, it answers `401` like the API:
|
|
214
|
+
Any key with the shape of a real one works: `Authorization: Bearer <id>|fc_live_<letters and digits>`, for example `1|fc_live_sandbox`. It belongs to the default workspace, unless it was minted for another one ([workspaces](#workspaces)); `X-Workspace-Id` is ignored, as in production. Without a key, or with a malformed one, it answers `401` like the API:
|
|
109
215
|
|
|
110
216
|
```json
|
|
111
217
|
{ "error": "unauthenticated", "message": "Falta la API key o no es válida." }
|
|
112
218
|
```
|
|
113
219
|
|
|
114
|
-
Every key
|
|
220
|
+
Every key has every permission.
|
|
115
221
|
|
|
116
222
|
**Every response carries `X-Request-Id`**, like the API: a different UUID v4 per request, also on errors (`401`, `404 route_not_found`, `422`…) and on the `/_sandbox` routes. It is only in the header, never in the body, and one you send in the request is ignored. The panel shows it next to each response, so you can quote it as you would quote the API's.
|
|
117
223
|
|
|
@@ -146,10 +252,12 @@ Every session starts with this:
|
|
|
146
252
|
| Contact «Contacto de ejemplo» | `2001` | Phone `5491100000000`. |
|
|
147
253
|
| Tag «Cliente» | `4001` | |
|
|
148
254
|
| Template `bienvenida`, approved, of number 3 | `45` | One variable: «Hola {{1}}, gracias por escribirnos…». |
|
|
255
|
+
| Group «Grupo de ejemplo» of number 4 | `7001`, `120363000000000000@g.us` | Number 4 is its admin; contact 2001, a participant. |
|
|
256
|
+
| Workspace «Principal» | `1` | The default one: every key belongs to it unless it was minted for another. |
|
|
149
257
|
|
|
150
258
|
`POST /_sandbox/reset` goes back to this and cancels everything that was scheduled.
|
|
151
259
|
|
|
152
|
-
**With state** (what you create, change or delete stays): contacts (`/api/v1/contacts…`, with their tags, fields and the `meta-window`), conversations (`/api/v1/conversations…`), messages (send, history, reactions, forward, cards
|
|
260
|
+
**With state** (what you create, change or delete stays): contacts (`/api/v1/contacts…`, with their tags, fields and the `meta-window`), conversations (`/api/v1/conversations…`), messages (send, history, reactions, forward, cards, the lookup by id, mark as read and «typing…»), tags, custom fields, templates, [WhatsApp forms](#whatsapp-forms), numbers and sign-up links ([connecting numbers](#connecting-numbers)), [groups](#groups) (the list, one group and the send) and webhook subscriptions with their history, in one or more [workspaces](#workspaces).
|
|
153
261
|
|
|
154
262
|
**The numbers** are those of accounts: `GET /api/v1/devices` always paginates (`{data, links, meta}`, `perPage` from 1 to 100), and each number carries `capabilities`, with what the API can do through it:
|
|
155
263
|
|
|
@@ -169,7 +277,7 @@ The official number (`3`) has no groups (the sandbox account is not in the offic
|
|
|
169
277
|
{ "error": "route_not_available", "message": "Esta ruta no está disponible para API keys en el piloto. No es un permiso que falte: ninguna key la habilita." }
|
|
170
278
|
```
|
|
171
279
|
|
|
172
|
-
**Everything else** in the spec (broadcasts, flows, kanban, groups, campaigns, alerts…) answers an example built from the response schema: the spec's operations carry no examples of their own. The key, the route, the query and the body are still validated.
|
|
280
|
+
**Everything else** in the spec (broadcasts, flows, kanban, the rest of groups, campaigns, alerts…) answers an example built from the response schema: the spec's operations carry no examples of their own. The key, the route, the query and the body are still validated.
|
|
173
281
|
|
|
174
282
|
**Validation** follows the spec and reads values like Laravel (`"5"` works as an integer, `"1"` as a boolean). A validation error is a `422` with Laravel's messages per field; a route that does not exist, `404 route_not_found`:
|
|
175
283
|
|
|
@@ -184,7 +292,7 @@ The official number (`3`) has no groups (the sandbox account is not in the offic
|
|
|
184
292
|
}
|
|
185
293
|
```
|
|
186
294
|
|
|
187
|
-
**What does not exist in the account** answers like each domain: in conversations (contacts, messages, tags, fields, forms) `404 {"message": "Objeto no encontrado."}`, without `error`; in accounts (numbers) `404 {"message": "Object not found"}`.
|
|
295
|
+
**What does not exist in the account** answers like each domain: in conversations (contacts, messages, tags, fields, forms) `404 {"message": "Objeto no encontrado."}`, without `error`; in accounts (numbers) `404 {"message": "Object not found"}`; in communities (a group by its uid) Laravel's `404 {"message": "No query results for model [App\\Modules\\Shared\\Models\\Group] <uid>"}`. **What is in another workspace** answers `404` too: `{"message": ""}` in conversations, `{"message": "Not Found"}` for a number, the same `404` as one that does not exist for links, subscriptions and groups.
|
|
188
296
|
|
|
189
297
|
The complete spec the sandbox uses is at `GET /_sandbox/openapi.json`.
|
|
190
298
|
|
|
@@ -320,6 +428,117 @@ curl -s http://localhost:4010/_sandbox/forms/complete -H 'Content-Type: applicat
|
|
|
320
428
|
|
|
321
429
|
It emits `whatsapp_form.completed` once per send, with `form` (`id` is the `family_id`, `version_id` the version sent), `message_id`, `contact_id`, `device_id` and `answers` (`{key, label, value}`, one per question, in order). What you do not send in `answers` takes an example value; options go by their id. `run_id` also works, or `contact_id` (the last pending send to that contact).
|
|
322
430
|
|
|
431
|
+
## Connecting numbers
|
|
432
|
+
|
|
433
|
+
Create and connect WhatsApp numbers through the API: a number by QR, or a sign-up link your customer opens. It is in
|
|
434
|
+
production in accounts since 25-10; the sandbox follows the contract of 22-10, its amendments v0.1 and v0.2 of 23-10
|
|
435
|
+
and the changes of 25-10 (the QR every 5 seconds, `country`), checked against accounts v1.5.97 (see
|
|
436
|
+
[Pending of the spec](#pending-of-the-spec)). A number, a link and the number a link creates belong to the workspace
|
|
437
|
+
of the key that created them.
|
|
438
|
+
|
|
439
|
+
**The official sign-up (`type: meta`) is not available in production yet:** it stays off until the domain is
|
|
440
|
+
registered with Meta, and `POST /api/v1/connect-links` with `type: meta` answers `503 meta_connect_disabled`. The sandbox
|
|
441
|
+
starts the same way. To test it, turn it on with `--meta-connect`, `"numbers": {"metaConnect": true}` in the file, or
|
|
442
|
+
`POST /_sandbox/numbers {"meta_connect": true}` while it runs.
|
|
443
|
+
|
|
444
|
+
| Route | What it does |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `POST /api/v1/devices` | A number by QR: `{"connection": "qr"}`, optional `alias`, `external_reference`, `same_number_as` and `country` (two letters, any case: the country of the phone, where the WhatsApp session is opened from). `201 {"data": <number>}` with `connected: 0` and no phone yet. |
|
|
447
|
+
| `GET /api/v1/devices/{device}/qr` | `{"data": {"connected", "qr_code", "expires_in"}}`: a PNG as a data URI, `null` and `expires_in: 0` once connected, with `Cache-Control: no-store`. `expires_in` is `5`: ask for the QR every 5 seconds and show the new one at once, since WhatsApp changes the code and an old one does not link. It is fake: nothing scans it. An official number answers `422 provider_unsupported`. |
|
|
448
|
+
| `GET /api/v1/devices?external_reference=…` | Only the numbers with exactly that reference. `PUT /api/v1/devices/{device}` changes it (`null` clears it). |
|
|
449
|
+
| `DELETE /api/v1/devices/{device}` | `200` with an empty body: deletes it, frees its place and emits `device.deleted`. |
|
|
450
|
+
| `POST /api/v1/connect-links` | A sign-up link, `201 {"data": <link>}` with `Cache-Control: no-store`. The `url` with the token is only in this answer. |
|
|
451
|
+
| `GET /api/v1/connect-links`, `GET …/{link}` | The links without their token, newest first, paginated, filtered by `status` and `external_reference`. The `id` is an integer. `status`: `pending`, `completed`, `failed`, `expired` (`error_code: link_expired`) or `revoked` (`error_code: revoked`). |
|
|
452
|
+
| `DELETE /api/v1/connect-links/{link}` | Revokes a `pending` link: `204`, `409 link_not_pending` otherwise, `404` if it does not exist. |
|
|
453
|
+
|
|
454
|
+
**The body of a link**, by `type`:
|
|
455
|
+
|
|
456
|
+
| Field | `type: qr` | `type: meta` |
|
|
457
|
+
|---|---|---|
|
|
458
|
+
| `allowed_connection_types` | prohibited | required: `coexistence`, `dedicated` or both, without repeating |
|
|
459
|
+
| `provision_us_number` | prohibited (even `false`) | optional: a US number of the pool (3 per account, 1 on trial accounts). With `true`, `allowed_connection_types` must include `dedicated` and `device_id` is not allowed: a pool number is always dedicated, and new |
|
|
460
|
+
| `same_number_as` | optional, not with `device_id`: QR on a number in coexistence | prohibited |
|
|
461
|
+
| `country` | optional, not with `device_id`: two letters, the country of the phone; the link returns it in lower case | prohibited |
|
|
462
|
+
| `device_id` | optional: reconnects that number (`422 connection_type_mismatch` if it is not QR) | the same, for an official number |
|
|
463
|
+
| `external_reference` | optional, up to 255 | optional, up to 255 |
|
|
464
|
+
| `return_url` | optional, `https`, up to 2048 | the same |
|
|
465
|
+
| `expires_in_minutes` | 30 to 10080, 1440 by default | the same |
|
|
466
|
+
|
|
467
|
+
`null` counts as missing, like Laravel's `prohibited`. A broken rule answers
|
|
468
|
+
`422 {"error": "invalid_request", "message": "…", "errors": {"field": ["…"]}}`, and so does `POST /devices`. A
|
|
469
|
+
`device_id` or `same_number_as` that is not a number of the account answers `422 invalid_reference_device`.
|
|
470
|
+
|
|
471
|
+
**Each number has** `external_reference` (your text, not unique) and `connection` (`qr`, `coexistence` or
|
|
472
|
+
`dedicated`). The four `device.*` events carry both inside `device`, and `connect_link_id`: in `device.connected`, the
|
|
473
|
+
link of that connection; in the other three, the link that connected the number last; `null` without one. `device.disconnected` brings `reason`: `qr_logged_out`, `meta_account_not_registered`,
|
|
474
|
+
`shadow_ban_protection`, `provider_error`, `unknown` or `disconnected_by_user`. Like every event, they end with
|
|
475
|
+
`workspace_id`, the number's workspace.
|
|
476
|
+
|
|
477
|
+
**The quota** counts phones: QR and Meta of the same phone count once. The sandbox account allows 5 and the two
|
|
478
|
+
example numbers use two; change it with `POST /_sandbox/numbers`. The rejections:
|
|
479
|
+
|
|
480
|
+
- `403 numbers_not_included`, and `403 number_limit_reached` with `limit`, `used` and `pending_device_ids`;
|
|
481
|
+
- `409 pending_number_exists` with `device_id`: there is already a QR number not connected yet, so connect it or
|
|
482
|
+
delete it first;
|
|
483
|
+
- `422 qr_requires_coexistence` (QR on an official number that is not in coexistence) and
|
|
484
|
+
`422 method_already_present` (with `device_id`).
|
|
485
|
+
|
|
486
|
+
A reconnection, and QR on a number in coexistence, take no place. Up to 20 open links (`429 too_many_open_links`)
|
|
487
|
+
and 10 creations per minute (Laravel's `429 {"message": "Too Many Attempts."}`, with `Retry-After`). While the Meta
|
|
488
|
+
sign-up is off (the default), a `meta` link answers `503 meta_connect_disabled`, and so does the page of a `meta` link
|
|
489
|
+
made while it was on.
|
|
490
|
+
|
|
491
|
+
**`Idempotency-Key`** works on both `POST`, per resource: the same key with the same body gives back the same `2xx`
|
|
492
|
+
with `Idempotent-Replayed: true`, without creating anything; with another body, `422 idempotency_key_mismatch`; a
|
|
493
|
+
malformed key (empty, over 255 or with spaces), `422 invalid_idempotency_key`. A rejection is not kept: fix it and
|
|
494
|
+
retry with the same key.
|
|
495
|
+
|
|
496
|
+
A number by QR, its code and the scan (the scan is a sandbox route):
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
KEY='Authorization: Bearer 1|fc_live_sandbox'
|
|
500
|
+
curl -s -X POST localhost:4010/api/v1/devices -H "$KEY" -H 'Content-Type: application/json' \
|
|
501
|
+
-H 'Idempotency-Key: alta-cliente-8841' -d '{"connection": "qr", "external_reference": "cliente-8841"}'
|
|
502
|
+
curl -s localhost:4010/api/v1/devices/5/qr -H "$KEY" # {"data": {"connected": false, "qr_code": "data:image/png;base64,…", "expires_in": 5}}
|
|
503
|
+
curl -s -X POST localhost:4010/_sandbox/devices/5/connect # device.connected
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
A sign-up link and its page (`meta` links need the Meta sign-up on: `npx funnelchat-sandbox --meta-connect`):
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
curl -s -X POST localhost:4010/api/v1/connect-links -H "$KEY" -H 'Content-Type: application/json' \
|
|
510
|
+
-d '{"type": "meta", "allowed_connection_types": ["coexistence", "dedicated"], "external_reference": "cliente-8841", "return_url": "https://your-app.example/connected"}'
|
|
511
|
+
# {"data": {"id": 1, "type": "meta", "status": "pending", "url": "http://localhost:4010/connect/<token>", …}}
|
|
512
|
+
curl -si -X POST 'http://localhost:4010/connect/<token>/start' -d connection=coexistence
|
|
513
|
+
# 303 Location: https://your-app.example/connected?connect_link_id=1&status=completed&device_id=6
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**The page** is a test page. Opening `GET /connect/{token}` does not use the link, as in production, so a
|
|
517
|
+
messenger's preview cannot burn it: it shows a button (and, for a `meta` link that allows both, the choice of
|
|
518
|
+
connection) and takes the token out of the address bar (`/connect/reopen`). The button, `POST /connect/{token}/start`,
|
|
519
|
+
connects at once: it creates the number (or reconnects it), marks the link `completed`, emits `device.connected` and
|
|
520
|
+
redirects to `return_url` with `connect_link_id`, `status`, `device_id` (if it completed) and `error_code` (if it
|
|
521
|
+
failed), or says it on the page. `POST /connect/{token}/complete` does the same. A link works once; then the page
|
|
522
|
+
answers `410`.
|
|
523
|
+
|
|
524
|
+
Two answers of the page do not end the link. A `connection` the link does not allow answers
|
|
525
|
+
`422 connection_type_not_allowed`. A failure of Facebook or Meta on a `meta` link (`error_code=facebook_auth_failed`
|
|
526
|
+
or `meta_error`) leaves it `pending` so your customer retries: `{"state": "retry", "error_code": "…", "attempts_left": 3}`
|
|
527
|
+
when you send JSON, the page with the button again otherwise; the fifth failure ends it `failed`. `quota_exceeded`,
|
|
528
|
+
`trial_number_blocked`, `number_not_owned` and the QR's `qr_timeout` end it at the first. In the body (form or JSON) or the query you choose what happens: `outcome=failed` with `error_code`
|
|
529
|
+
(`qr_timeout` or `meta_error` by default), `connection` when the link allows both, and `number`. With `Authorization`
|
|
530
|
+
or `X-Api-Key` it answers `403 credentials_not_allowed`. Its headers are production's: `Cache-Control: no-store, private`,
|
|
531
|
+
`Referrer-Policy: no-referrer`, `X-Frame-Options: DENY`, `X-Robots-Tag: noindex`, HSTS, COOP and a CSP with a nonce.
|
|
532
|
+
|
|
533
|
+
With the Node SDK, once it has the new methods:
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
const device = await funnelchat.devices.create({ connection: 'qr', external_reference: 'cliente-8841' });
|
|
537
|
+
const { qr_code } = await funnelchat.devices.qr(device.id);
|
|
538
|
+
const link = await funnelchat.connectLinks.create({ type: 'qr', return_url: 'https://your-app.example/connected' });
|
|
539
|
+
await fetch(`${link.url}/start`, { method: 'POST', redirect: 'manual' }); // the button of the sandbox page
|
|
540
|
+
```
|
|
541
|
+
|
|
323
542
|
## Webhooks
|
|
324
543
|
|
|
325
544
|
You subscribe with the same routes the SDKs and the n8n trigger use (the unified subscriptions of accounts):
|
|
@@ -333,7 +552,7 @@ You subscribe with the same routes the SDKs and the n8n trigger use (the unified
|
|
|
333
552
|
| `POST /api/v1/webhooks/{id}/rotate-secret` | A new secret; the previous one stops working. |
|
|
334
553
|
| `POST /api/v1/webhooks/{id}/test` | Sends a signed `webhook.test` now and answers `{succeeded, response_status, error, duration_ms, delivery_id}`. |
|
|
335
554
|
| `GET /api/v1/webhooks/{id}/deliveries` | The history: one record per attempt, newest first. |
|
|
336
|
-
| `GET /api/v1/webhooks/events` | The catalog of accounts in production:
|
|
555
|
+
| `GET /api/v1/webhooks/events` | The catalog of accounts in production: 59 types (54 `active`), with `area`, `status`, `is_critical`, `label`, `description` and `example`. |
|
|
337
556
|
|
|
338
557
|
**Each delivery** is a `POST` with the portal's envelope and these headers:
|
|
339
558
|
|
|
@@ -354,11 +573,13 @@ X-Funnelchat-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
|
|
|
354
573
|
"message": { "id": 9002, "type": "text", "text": "Hola, quiero más información", "media": null },
|
|
355
574
|
"contact": { "id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000" },
|
|
356
575
|
"receiver": { "phone": "+5491100000001" },
|
|
357
|
-
"device_id": 3
|
|
576
|
+
"device_id": 3,
|
|
577
|
+
"workspace_id": 1
|
|
358
578
|
}
|
|
359
579
|
}
|
|
360
580
|
```
|
|
361
581
|
|
|
582
|
+
- **`data.workspace_id`** is in every event, the last key of `data`: the workspace the event belongs to (the number's, the contact's or the group's). A subscription belongs to the workspace of the key that created it and only gets that workspace's events; `webhook.test` carries the subscription's.
|
|
362
583
|
- **The signature** is the portal's: `v1` is the HMAC-SHA256, with the full secret (with `whsec_`), of `"<t>." + raw body`. Verify it with `Funnelchat.webhooks.verify` (Node), `Webhook::verify` (PHP) or the portal's snippets. The sandbox's tests verify it with the Node SDK's `verify` and with the portal's `verificar-firma.js`, on the vectors of `tests/test_firmas.py`.
|
|
363
584
|
- **The retries** are the portal's: up to 4 attempts, waiting 60 s, 300 s and 1800 s (shorten them with `--retry-delays`). A network error, a timeout (10 s), a `5xx` or a `429` is retried; any other `4xx` or a `3xx` is final. Every attempt carries the same body and the same `id`, a new signature and another `X-Funnelchat-Delivery`.
|
|
364
585
|
- **Five failed deliveries in a row** disable the subscription (`status: disabled`, `disabled_reason: too_many_failures`), as in accounts. A `PUT` with `"status": "active"` turns it back on.
|
|
@@ -372,20 +593,24 @@ These routes take no key: they belong to the sandbox, not to the API.
|
|
|
372
593
|
| Route | What it simulates |
|
|
373
594
|
|---|---|
|
|
374
595
|
| `POST /_sandbox/inbound/message` | A contact writes to you: `{"phone": "5491100000000", "device_id": 3, "text": "Hola"}` (or `contact_id`; optional `name`, `type`, `file` and `occurred_at`). If the phone is new it creates the contact (`contact.created`); then it emits `message.received` and opens the 24-hour window of that number. |
|
|
375
|
-
| `POST /_sandbox/groups
|
|
596
|
+
| `POST /_sandbox/groups`, `GET /_sandbox/groups` | A group of a connected QR number, its admin, in the number's workspace: `{"device_id": 5, "name": "Clientes", "participants": [{"phone": "5491155550000", "name": "Ana"}]}`. |
|
|
597
|
+
| `POST /_sandbox/groups/participant-joined`, `…/participant-left` | Someone joins or leaves a group: `{"group_id": 7001, "participant": {"phone": "…", "name": "…"}}` (or `"uid"`; the example group without either). The group's participants change and the event goes to its workspace. A `group` the sandbox does not have (`{"group": {"uid": "…@g.us", "name": "…"}}`) goes out from the catalog's example. |
|
|
376
598
|
| `POST /_sandbox/forms/complete` | The contact completes a sent form: emits `whatsapp_form.completed` (see [WhatsApp forms](#whatsapp-forms)). |
|
|
377
599
|
| `POST /_sandbox/mac`, `GET /_sandbox/mac` | The account's MAC: `{"lockout": true, "active_contact_ids": [2001], "inactive_contact_ids": []}`. With `lockout`, sends to contacts that are not active answer `429 mac_lockout_24h`; an inactive contact always gets it. |
|
|
378
600
|
| `POST /_sandbox/contacts/{id}/tags` with `{"tag_id": 4001}`, `DELETE /_sandbox/contacts/{id}/tags/{tag}` | Someone tags the contact in the panel: `contact.tag_added` and `contact.tag_removed`. |
|
|
379
|
-
| `POST /_sandbox/devices/{id}/disconnect`, `…/connect` | The number disconnects or connects: `device.disconnected` and `device.connected`. Disconnected, sends answer `422 device_unavailable`. |
|
|
601
|
+
| `POST /_sandbox/devices/{id}/disconnect`, `…/connect` | The number disconnects (`{"reason": "disconnected_by_user"}`, any of the list; `qr_logged_out` for QR and `unknown` for official ones by default) or connects, which is the scan of a QR (`{"number": "5491155550101"}`, a fake one otherwise): `device.disconnected` and `device.connected`. Disconnected, sends answer `422 device_unavailable`. |
|
|
602
|
+
| `POST /_sandbox/devices/{id}/health` | The health of an official number changes: `{"state": "limited"}` (`available`, `limited`, `blocked`, `unknown`) emits `device.health_changed`, only if it changed. |
|
|
603
|
+
| `POST /_sandbox/numbers`, `GET /_sandbox/numbers` | The quota of the account: `{"limit": 3, "included": true, "trial": false, "meta_connect": true, "links_per_minute": 10}` (`meta_connect` turns the Meta sign-up on or off). `GET` also says how many places are in use and which QR numbers are pending. |
|
|
380
604
|
| `POST /_sandbox/templates/{id}/status` | Meta approves or rejects a template: `{"status": "REJECTED", "reason": "…"}` emits `template.status_changed`. |
|
|
381
|
-
| `POST /_sandbox/events/{type}` | Any event of the catalog, with its example; what you send in `{"data": {…}}` overrides it. `GET /_sandbox/events` lists them. `group.message_received` and `group.reaction_added` go out with the shape communities emits (`message.uid` and every key of the message), not with the example of the accounts catalog, which uses `message.id`. |
|
|
605
|
+
| `POST /_sandbox/events/{type}` | Any event of the catalog, with its example; what you send in `{"data": {…}}` overrides it, and `{"workspace_id": 2}` sends it to that workspace (the default one otherwise). `GET /_sandbox/events` lists them. `group.message_received` and `group.reaction_added` go out with the shape communities emits (`message.uid` and every key of the message), not with the example of the accounts catalog, which uses `message.id`. |
|
|
382
606
|
| `POST /_sandbox/window` | Opens or closes the window by hand: `{"contact_id": 2001, "device_id": 3, "open": false}`. |
|
|
607
|
+
| `POST /_sandbox/workspaces`, `GET /_sandbox/workspaces` | A [workspace](#workspaces) with a key that only sees it: `{"name": "Cliente B"}` answers `201 {workspace, key}`. `POST /_sandbox/keys` with `{"workspace_id": 2}` mints another key (of the default workspace without `workspace_id`). |
|
|
383
608
|
| `POST /_sandbox/reset` | Back to the initial state. |
|
|
384
|
-
| `GET /_sandbox/state`, `/_sandbox/requests`, `/_sandbox/deliveries` | The state, the last requests
|
|
609
|
+
| `GET /_sandbox/state`, `/_sandbox/requests`, `/_sandbox/deliveries`, `/_sandbox/emitted` | The state, the last requests, the last deliveries with what your server answered, and every event emitted, delivered or not (with how many subscriptions got it). |
|
|
385
610
|
|
|
386
611
|
Besides, as in the API: creating a contact emits `contact.created`, closing a conversation (`active_conversation: false`) emits `conversation.closed`, assigning it emits `conversation.assigned`, and a template created through the API goes from `PENDING` to `APPROVED` after 5 seconds with `template.status_changed`.
|
|
387
612
|
|
|
388
|
-
**The panel** is at `http://localhost:4010/_sandbox`: the last requests, the webhook deliveries with your server's answer, and a form to send an incoming message.
|
|
613
|
+
**The panel** is at `http://localhost:4010/_sandbox`: the numbers (with **Scan the QR** for one waiting for it), every event emitted, the last requests, the webhook deliveries with your server's answer, and a form to send an incoming message.
|
|
389
614
|
|
|
390
615
|
## End to end
|
|
391
616
|
|
|
@@ -459,44 +684,49 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
|
|
|
459
684
|
## What it does not imitate
|
|
460
685
|
|
|
461
686
|
- **It sends nothing to WhatsApp** and charges nothing: the `sent`, `delivered` and `read` statuses are made up by the sandbox, with the delays you configure. In the real API, a message still `queued` after an hour reads as `status: null`; in the sandbox it always moves on.
|
|
462
|
-
- **One account and every permission:** any well-formed key gets in, is never revoked and there is no `403 permission_denied`. API keys are not created here, and subscriptions are not filtered by the key's areas (the real API answers `422` on `events` if the key lacks the event's `{area}:read`). The only `403 route_not_available` is the one of the routes no key enables.
|
|
687
|
+
- **One account and every permission:** any well-formed key gets in, is never revoked and there is no `403 permission_denied`. Workspaces are created and their keys minted through `/_sandbox`, never deleted, and a number does not move between workspaces. API keys are not created here, and subscriptions are not filtered by the key's areas (the real API answers `422` on `events` if the key lacks the event's `{area}:read`). The only `403 route_not_available` is the one of the routes no key enables.
|
|
463
688
|
- **There are no daily call caps** (`429 daily_cap_exceeded`), no `503 gate_unavailable` and no balance: to test those cases, force the failure. Of the MAC there is the lockout (`429 mac_lockout_24h`); `429 mac_limit_reached` is only forced, and the real API also stores in the history an error row for every send stopped by the MAC, which the sandbox does not.
|
|
464
689
|
- **The state is memory only:** it is lost when the process stops or with `POST /_sandbox/reset`. So your receiver does not lose the secret, put the subscription in the configuration file.
|
|
465
690
|
- **The routes without state** answer an example built from the schema, always the same, even if it does not match your data.
|
|
466
691
|
- **The validation messages** are Laravel's in Spanish, built from the schema: they may not match the API's word for word (decide by the HTTP status and by `error`, not by `message`). The real API's validation `422` is Laravel's, `{message, errors}`, without `error`; the sandbox adds `"error": "validation_failed"`.
|
|
467
692
|
- **Webhook URLs** may be `http` and local, without the real API's protection against internal destinations (try `--require-https`). Do not expose the sandbox to the internet: the `/_sandbox` routes take no key and the sandbox `POST`s to any URL subscribed to it.
|
|
468
|
-
- **The catalog** is
|
|
469
|
-
- **
|
|
470
|
-
- **
|
|
693
|
+
- **The catalog** is rendered from the code of accounts v1.5.97 (`spec/catalog.json`, with `scripts/transcribe-catalog.php`), not read from a response: if accounts changes it, it has to be rendered again.
|
|
694
|
+
- **Groups have state only for the list, one group and the send of a message** (the action is `completed` after the delays of `sent` and `delivered`; it never fails on its own). The other group actions, the campaigns and the rest of the groups' routes answer examples, and their events are fired from `/_sandbox`. There are no official groups (the account is not in their beta), no `too_many_pending_actions` and no rate limit of 30 sends per minute.
|
|
695
|
+
- **Workspaces confine** numbers, sign-up links, webhook subscriptions, contacts, conversations, messages, templates and groups. Tags, custom fields and forms are not confined in the sandbox. A contact belongs to one workspace, and the phone stays unique in the account: `POST /api/v1/contacts` with a phone of another workspace gives back that contact (`200`), as conversations v1.19.7 does.
|
|
696
|
+
- **Forms do not talk to Meta:** `sync` and `publish` succeed unless the linter finds an error, and the linter is a short version of the real one (screens without a title, without an ending, without questions, with a connection to the server). The metrics (`…/metrics`) answer the example of their schema, and there are no dynamic forms.
|
|
471
697
|
- **Files are not downloaded:** the type comes from the extension or from `file_type`. The «known» extensions (the ones that give `415` instead of `422 errors.file_type`) are the common ones; the API uses symfony/mime's full list.
|
|
698
|
+
- **Connecting numbers** follows a contract, not production: no real QR, no Meta Embedded Signup and no 15-minute hold of a pool number. The page has no `POST /connect/{token}/qr` (the button connects at once) and no `POST /connect/{token}/offer` (the pool number is given when the link completes). A QR link that fails does not create (and delete) a number, links do not die when a key is revoked, the per-minute limit counts only the calls that pass the schema, and `409 operation_in_progress` only appears if two calls with the same `Idempotency-Key` overlap. The errors that depend on the plan, the payment or the provider (`connect_not_allowed`, `number_suspended_by_plan`, `payment_required`, `device_inactive`, `provider_error`…) are documented but never answered.
|
|
699
|
+
- **Template metrics and the WhatsApp Business accounts** (`GET /api/v1/templates/{template}/metrics`, `GET /api/v1/me/whatsapp-business-accounts…`) answer the example of their schema, not data of the sandbox. **Mark as read and «typing…»** have no per-second ceilings (`429 rate_limited`) and never fail at WhatsApp (`502 provider_error`).
|
|
700
|
+
- **`POST /api/v1/webhooks/{id}/rotate-secret` works with a key here**, as it always did in the sandbox; production keeps it out of the keys' routes (the secret is rotated from the panel), so a key gets `403 route_not_available` there.
|
|
701
|
+
- **Forms** are served here, but the API edge does not route `forms` yet: through the public URL they answer `404 route_not_found`.
|
|
472
702
|
- **The pace and the brakes** use the numbers of conversations in production (`config/send_guards.php`, `config/antiblock.php`); the official number paired with a QR one in coexistence (15 per second, shared) is not there.
|
|
473
703
|
|
|
474
704
|
## Pending of the spec
|
|
475
705
|
|
|
476
|
-
The spec
|
|
706
|
+
The spec (`spec/funnelchat.json`) is the portal's full unified spec of 04-11 (developers-portal `c90565e4`, v0.6.0), made from accounts v1.5.97, conversations v1.19.7 and communities v2.35.5: every route an API key reaches, not only the ones the portal shows. What production has and that spec lacks is in two separate layers, which the next regeneration drops on its own once the spec carries it:
|
|
477
707
|
|
|
478
|
-
- `spec/pending.json`: the pending layer of the Node SDK, copied unchanged (`funnelchat-node` `feat/
|
|
708
|
+
- `spec/pending.json`: the pending layer of the Node SDK, copied unchanged (`funnelchat-node` `feat/primer-uso` @ `8469a5c`). It brings what the spec lacks (the forms, the send to a group, `rotate-secret`, the accounts and communities events) and, guarded by JSON Pointer conditions, corrections where the spec says otherwise than production answers (the send answers, `devices/{device}/qr`, the template list, `PUT webhooks/{id}`, and `message.received`, `message.status_changed` and `whatsapp_form.completed`).
|
|
479
709
|
- `spec/pending-sandbox.json`: what the sandbox adds, with its sources inside.
|
|
480
710
|
|
|
481
|
-
Besides, `spec/catalog.json` is the event catalog of accounts v1.
|
|
711
|
+
Besides, `spec/catalog.json` is the event catalog of accounts v1.5.97, with its provenance.
|
|
482
712
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
|
486
|
-
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
| `capabilities` and the new keys of the numbers | accounts v1.4.24 (`DeviceResource`, `DeviceCapabilities`) | In production. |
|
|
491
|
-
| `X-Request-Id` and the edge's errors (`route_not_found`, `origin_unreachable`, `edge_misconfigured`) | api-edge v0.3.0, platform-gate v0.8.0 | In production. |
|
|
713
|
+
What `spec/pending-sandbox.json` still adds:
|
|
714
|
+
|
|
715
|
+
| What | Why neither the spec nor the Node SDK's layer has it |
|
|
716
|
+
|---|---|
|
|
717
|
+
| Polls and events, `POST /api/v1/contacts/{contact}/message/poll` and `…/event` | Closed to every key by the gate: they are kept to answer its `403 route_not_available`, not a `404` |
|
|
718
|
+
| `data.workspace_id` in every event (accounts v1.5.97, Q35) | The events of the spec do not carry it yet |
|
|
719
|
+
| `null` in `allowed_connection_types` and `provision_us_number` of `POST /api/v1/connect-links` | A correction: accounts' rules depend on `type`, and Laravel's `prohibited` lets `null` through |
|
|
492
720
|
|
|
493
721
|
## Regenerating
|
|
494
722
|
|
|
495
723
|
Everything generated comes from a single script:
|
|
496
724
|
|
|
497
725
|
```bash
|
|
498
|
-
# With a new
|
|
499
|
-
|
|
726
|
+
# With a new full spec of the portal (`npm run build` in developers-portal writes generated/specs/funnelchat-completa.json;
|
|
727
|
+
# never the published public/specs/funnelchat.json, which carries only the visible operations). It is a build
|
|
728
|
+
# artifact, so --origin says where it comes from (repository, branch, commit, path and the tag of each domain):
|
|
729
|
+
npm run generate -- --spec ../developers-portal/generated/specs/funnelchat-completa.json --origin origin.json
|
|
500
730
|
|
|
501
731
|
# With a new pending layer of the Node SDK:
|
|
502
732
|
npm run generate -- --pending ../funnelchat-node/spec/pending.json
|
|
@@ -508,13 +738,15 @@ npm run generate
|
|
|
508
738
|
npm run generate:check
|
|
509
739
|
```
|
|
510
740
|
|
|
741
|
+
The event catalog (`spec/catalog.json`) is rendered from accounts' code with `scripts/transcribe-catalog.php` (PHP 8.2 or later, no Laravel needed); its header says how.
|
|
742
|
+
|
|
511
743
|
What it does:
|
|
512
744
|
|
|
513
|
-
1. With `--spec` or `--pending`, it copies the file to `spec/` and writes in `spec/procedencia.json` the repository, the branch, the commit, the date and the `sha256
|
|
745
|
+
1. With `--spec` or `--pending`, it copies the file to `spec/` and writes in `spec/procedencia.json` the repository, the branch, the commit, the date and the `sha256` (from git, or from `--origin` for what git cannot say).
|
|
514
746
|
2. It applies over the snapshot `spec/pending.json` and then `spec/pending-sandbox.json`, but **only what the snapshot lacks**. At the end it lists what the snapshot already carries, so you can delete it.
|
|
515
747
|
3. It writes `generated/openapi.json`, the spec the sandbox validates and answers with.
|
|
516
748
|
|
|
517
|
-
**The current
|
|
749
|
+
**The current spec** is the portal's full unified spec of 04-11 (`main` @ `c90565e4`, `generated/specs/funnelchat-completa.json`): 203 paths, 248 operations and 29 events of accounts v1.5.97 (`558332bcf`), conversations v1.19.7 (`4e35ddb83`) and communities v2.35.5 (`2be913d4`). The detail is in `spec/procedencia.json`.
|
|
518
750
|
|
|
519
751
|
## Development
|
|
520
752
|
|
|
@@ -531,18 +763,19 @@ The tests verify the deliveries with unchanged copies of the Node SDK's `verify`
|
|
|
531
763
|
|
|
532
764
|
```
|
|
533
765
|
spec/ the spec, its provenance, the two pending layers and the event catalog
|
|
534
|
-
scripts/ generate.mjs, the only generator
|
|
766
|
+
scripts/ generate.mjs, the only generator, and transcribe-catalog.php for the catalog
|
|
535
767
|
generated/ the merged spec (not edited)
|
|
536
768
|
src/openapi/ routes, validation and examples from the spec
|
|
537
|
-
src/handlers/ the routes with state (contacts, messages, forms, webhooks) and the ones the gate closes
|
|
769
|
+
src/handlers/ the routes with state (numbers, links, contacts, messages, forms, groups, webhooks) and the ones the gate closes
|
|
538
770
|
src/forms/ the form templates
|
|
539
771
|
src/messaging/ send rules, the file by URL, forced failures and message statuses
|
|
540
772
|
src/webhooks/ signature, catalog, deliveries and retries
|
|
773
|
+
src/state/ the in-memory state, the public shapes and the workspaces
|
|
541
774
|
src/sandbox/ the /_sandbox routes and the panel
|
|
542
775
|
test/ vitest
|
|
543
776
|
```
|
|
544
777
|
|
|
545
|
-
**Publishing:** push a `vX.Y.Z` tag on `main` that matches the `version` in `package.json
|
|
778
|
+
**Publishing:** push a `vX.Y.Z` tag on `main` that matches the `version` in `package.json`; GitHub Actions publishes it through npm Trusted Publishing (`.github/workflows/publish.yml`).
|
|
546
779
|
|
|
547
780
|
## License
|
|
548
781
|
|