@funnelchat-hq/sandbox 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
- 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.
5
+ It imitates production on 05-11: accounts v1.5.97, conversations v1.19.21, communities v2.35.7, platform-gate v0.9.1 and the API edge v0.3.0.
6
6
 
7
7
  ## Try it in 3 minutes, without an account
8
8
 
@@ -31,22 +31,19 @@ curl -s -X POST http://localhost:4010/_sandbox/devices/5/connect
31
31
  # device.connected: number 5 is connected
32
32
  ```
33
33
 
34
- **2. Send a message.** Create the contact first, then send it a text through number 5:
34
+ **2. Send a message.** One call, by phone, through number 5: if the phone is not a contact yet, it becomes one.
35
35
 
36
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" \
37
+ curl -s -X POST $API/message/send-message -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
41
38
  -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", …}}
39
+ -d '{"device_id": 5, "phone": "5491155550000", "text": "Hola, Ana", "client_reference": "pedido-8841"}'
40
+ # {"message": {"id": 9001, "contact_id": 2002, "status": "queued", "client_reference": "pedido-8841", …}}
44
41
  ```
45
42
 
46
43
  **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
44
 
48
45
  ```bash
49
- curl -s $API/contacts/2002/messages/9001 -H "Authorization: Bearer $FUNNELCHAT_API_KEY"
46
+ curl -s $API/contacts/2002/messages/9001 -H "Authorization: Bearer $FUNNELCHAT_API_KEY" # 2002: the contact_id of the answer
50
47
  # {"id": 9001, "status": "read", …}
51
48
  ```
52
49
 
@@ -65,21 +62,25 @@ On an official number (`3`) this also opens the 24-hour window: free text only g
65
62
 
66
63
  ### Groups
67
64
 
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.
65
+ Groups start from your number's live list: `GET /api/v1/devices/{device}/groups` reads the groups the number is in on WhatsApp (QR numbers), and `device_id` says which ones the account already has. The sandbox's QR number `4` is in two: `120363000000000000@g.us` («Grupo de ejemplo», already in the account) and `120363000000000001@g.us` («Clientes VIP», not yet).
69
66
 
70
67
  ```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": {…}}
68
+ curl -s "$API/devices/4/groups" -H "Authorization: Bearer $FUNNELCHAT_API_KEY"
69
+ # {"data": [{"uid": "120363000000000000@g.us", "name": "Grupo de ejemplo", "device_id": 4, …}, {"uid": "120363000000000001@g.us", "name": "Clientes VIP", "device_id": "", …}], "meta": {…}}
73
70
  ```
74
71
 
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:
72
+ **Follow a group** to know who joins and leaves: `POST /api/v1/groups/{uid}/import` with `device_id` imports it to the account, as the panel does (`201`, or `200` if it already was there, with `is_admin` read from WhatsApp). From then on it is in `GET /api/v1/groups` and `GET /api/v1/groups/{uid}`, and announces `group_participant.joined` and `group_participant.left` (`group`, `campaign`, `null` without one, and `participant`, whose `identity` is the phone in digits). It needs the permission **Grupos y campañas → Administrar grupos** (`403 import_permission_required`), only QR numbers (`422 provider_unsupported`), and each number imports up to 5 groups per minute (`429 import_rate_limited`, with `Retry-After`).
76
73
 
77
74
  ```bash
75
+ curl -s -X POST "$API/groups/120363000000000001@g.us/import" -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
76
+ -H 'Content-Type: application/json' -d '{"device_id": 4}'
77
+ # 201 {"data": {"uid": "120363000000000001@g.us", "name": "Clientes VIP", "imported": true, "is_admin": true}}
78
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"}}'
79
+ -d '{"uid": "120363000000000001@g.us", "participant": {"phone": "5491155550000", "name": "Ana"}}'
80
+ # group_participant.joined (a group the account does not have announces nothing)
80
81
  ```
81
82
 
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
+ **Writing 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`. A group the account does not have is imported first, with the same rules (and the number must be its admin):
83
84
 
84
85
  ```bash
85
86
  curl -s -X POST "$API/groups/120363000000000000@g.us/messages" -H "Authorization: Bearer $FUNNELCHAT_API_KEY" \
@@ -87,7 +88,7 @@ curl -s -X POST "$API/groups/120363000000000000@g.us/messages" -H "Authorization
87
88
  # 202 {"action_id": "…", "status": "queued"}
88
89
  ```
89
90
 
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
+ `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). Two imports of the same group at a time cannot happen in the sandbox; `X-Sandbox-Reject: import_in_progress` gives production's `409`. A key minted with `POST /_sandbox/keys` and `"permissions": ["communities:send"]` lacks the permission to import. 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
 
92
93
  ### Workspaces
93
94
 
@@ -127,7 +128,7 @@ npx funnelchat-sandbox
127
128
  ```
128
129
 
129
130
  ```
130
- funnelchat-sandbox 0.3.0 at http://127.0.0.1:4010
131
+ funnelchat-sandbox 0.3.2 at http://127.0.0.1:4010
131
132
  API: http://127.0.0.1:4010/api/v1/... (Authorization: Bearer 1|fc_live_sandbox, or any well-formed key)
132
133
  panel: http://127.0.0.1:4010/_sandbox
133
134
  example numbers: 3 (official, with the 24-hour window) and 4 (QR, no window); contact 2001; template 45
@@ -252,7 +253,8 @@ Every session starts with this:
252
253
  | Contact «Contacto de ejemplo» | `2001` | Phone `5491100000000`. |
253
254
  | Tag «Cliente» | `4001` | |
254
255
  | 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
+ | Group «Grupo de ejemplo» of number 4 | `7001`, `120363000000000000@g.us` | In the account. Number 4 is its admin; contact 2001, a participant. |
257
+ | Group «Clientes VIP» of number 4 | `120363000000000001@g.us` | Only on WhatsApp, to follow (import). Number 4 is its admin. |
256
258
  | Workspace «Principal» | `1` | The default one: every key belongs to it unless it was minted for another. |
257
259
 
258
260
  `POST /_sandbox/reset` goes back to this and cancels everything that was scheduled.
@@ -298,6 +300,8 @@ The complete spec the sandbox uses is at `GET /_sandbox/openapi.json`.
298
300
 
299
301
  ## Sending a message
300
302
 
303
+ **By phone, without `contact_id`** (conversations v1.19.21): `POST /api/v1/message/send-message` and `POST /api/v1/devices/{device}/templates/{template}/send-template` take `phone` instead of `contact_id`. The contact of that phone is found, or created in the key's workspace (with the `422`s of `update-or-create` for a phone that is not one), and the answer's `message.contact_id` says which. The contact stays created even if a rule (the window, the brakes, the MAC, a disconnected number) then stops the send. A phone of a contact of another workspace answers `404` and creates nothing. `whatsapp_id` (Meta's BSUID, `US.13491208655302741918`) works instead of `phone` only with an official number; with a QR number, together with `phone` or without the shape of a BSUID, it is a `422` on `whatsapp_id`. With `contact_id`, a `phone` or `whatsapp_id` in the body must be that contact's: `422 recipient_mismatch`, on `send-message` too.
304
+
301
305
  ```bash
302
306
  curl -s http://localhost:4010/api/v1/contacts/2001/message \
303
307
  -H 'Authorization: Bearer 1|fc_live_sandbox' -H 'Content-Type: application/json' \
@@ -593,7 +597,7 @@ These routes take no key: they belong to the sandbox, not to the API.
593
597
  | Route | What it simulates |
594
598
  |---|---|
595
599
  | `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. |
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"}]}`. |
600
+ | `POST /_sandbox/groups`, `GET /_sandbox/groups` | A group of a connected QR number, in the number's workspace: `{"device_id": 5, "name": "Clientes", "participants": [{"phone": "5491155550000", "name": "Ana"}]}`. In the account unless `"imported": false` (only on WhatsApp, to import); the number is its admin unless `"admin": false`. |
597
601
  | `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. |
598
602
  | `POST /_sandbox/forms/complete` | The contact completes a sent form: emits `whatsapp_form.completed` (see [WhatsApp forms](#whatsapp-forms)). |
599
603
  | `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. |
@@ -604,7 +608,7 @@ These routes take no key: they belong to the sandbox, not to the API.
604
608
  | `POST /_sandbox/templates/{id}/status` | Meta approves or rejects a template: `{"status": "REJECTED", "reason": "…"}` emits `template.status_changed`. |
605
609
  | `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`. |
606
610
  | `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`). |
611
+ | `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`); with `"permissions": [...]` it has only those (the sandbox checks just `communities:operate`, to import a group). |
608
612
  | `POST /_sandbox/reset` | Back to the initial state. |
609
613
  | `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). |
610
614
 
@@ -691,8 +695,8 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
691
695
  - **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"`.
692
696
  - **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.
693
697
  - **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.
698
+ - **Groups have state only for the number's live list, the list, one group, the import and the send of a message** (the action is `completed` after the delays of `sent` and `delivered`; it never fails on its own). The live list is never cached (production keeps it a minute for a key). 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.
699
+ - **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`, `update-or-create` and a send by phone with a phone of another workspace answer `404`.
696
700
  - **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.
697
701
  - **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
702
  - **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.
@@ -703,9 +707,9 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
703
707
 
704
708
  ## Pending of the spec
705
709
 
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:
710
+ The spec (`spec/funnelchat.json`) is the portal's full unified spec of 05-11 (developers-portal `99928866`, v0.6.3), made from accounts v1.5.97, conversations v1.19.21 and communities v2.35.7 (with the send to a group and the import): 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:
707
711
 
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`).
712
+ - `spec/pending.json`: the pending layer of the Node SDK, copied unchanged (`funnelchat-node` `feat/enviar-por-telefono` @ `a8f8350`). It brings what the spec lacks (the forms, `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}`, `GET groups/actions/{action}`, and `message.received`, `message.status_changed` and `whatsapp_form.completed`).
709
713
  - `spec/pending-sandbox.json`: what the sandbox adds, with its sources inside.
710
714
 
711
715
  Besides, `spec/catalog.json` is the event catalog of accounts v1.5.97, with its provenance.
@@ -746,7 +750,7 @@ What it does:
746
750
  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.
747
751
  3. It writes `generated/openapi.json`, the spec the sandbox validates and answers with.
748
752
 
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`.
753
+ **The current spec** is the portal's full unified spec of 05-11 (`main` @ `99928866`, `generated/specs/funnelchat-completa.json`): 218 paths, 265 operations and 29 events of accounts v1.5.97 (`558332bcf`), conversations v1.19.21 (`f2937a71`) and communities v2.35.7 (`feat/grupos-sin-panel` @ `54643bbd`). The detail is in `spec/procedencia.json`.
750
754
 
751
755
  ## Development
752
756