@funnelchat-hq/sandbox 0.3.2 → 0.3.3
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 +5 -4
- package/dist/{chunk-MPXB6GIV.js → chunk-EQLGOYR6.js} +185 -10
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/spec/procedencia.json +15 -15
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
|
|
5
|
+
It imitates production on 06-11: accounts v1.5.97, conversations v1.19.37, 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
|
|
|
@@ -128,7 +128,7 @@ npx funnelchat-sandbox
|
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
```
|
|
131
|
-
funnelchat-sandbox 0.3.
|
|
131
|
+
funnelchat-sandbox 0.3.3 at http://127.0.0.1:4010
|
|
132
132
|
API: http://127.0.0.1:4010/api/v1/... (Authorization: Bearer 1|fc_live_sandbox, or any well-formed key)
|
|
133
133
|
panel: http://127.0.0.1:4010/_sandbox
|
|
134
134
|
example numbers: 3 (official, with the 24-hour window) and 4 (QR, no window); contact 2001; template 45
|
|
@@ -327,6 +327,7 @@ curl -s http://localhost:4010/api/v1/contacts/2001/message \
|
|
|
327
327
|
|
|
328
328
|
- **Queued is not sent.** Then, at 1, 3 and 6 seconds (configurable), the message moves to `sent`, `delivered` and `read`, and every change arrives through `message.status_changed` with the same `message_id`, `previous_status` (`null` on the first one) and your `client_reference`. `queued` never appears in the event.
|
|
329
329
|
- The current status is read with `GET /api/v1/contacts/{contact}/messages/{message}` (the same object) and in the history `GET /api/v1/contacts/{contactId}/messages`.
|
|
330
|
+
- **A template created in Meta that does not appear in `GET /api/v1/templates`:** bring it with `POST /api/v1/devices/{device}/templates` (official numbers only, conversations v1.19.37) and list again. In the sandbox nothing goes to Meta: an official number answers `202 {"status": "completed"}`, a QR one `422 number_not_connected_to_meta`, and a number that is not of the key's workspace `404`.
|
|
330
331
|
- The same goes for `POST /api/v1/message/send-message` (`{message}`), templates (`POST /api/v1/devices/{device}/templates/{template}/send-template`, `{message}`), forms (`POST /api/v1/contacts/{contact}/message/form`, `{message}`), cards (`{messages: [...]}`, one per card) and the forward (one item per destination, with its `message`, or `status: "blocked"` and `message: null`).
|
|
331
332
|
- `client_reference` is optional: 1 to 64 visible ASCII characters, no spaces. If it does not fit, `422 errors.client_reference`.
|
|
332
333
|
- A note (`"note": true`) is stored with `status: null` and is not sent.
|
|
@@ -707,7 +708,7 @@ And at `http://localhost:4010/_sandbox` you see each request and each delivery w
|
|
|
707
708
|
|
|
708
709
|
## Pending of the spec
|
|
709
710
|
|
|
710
|
-
The spec (`spec/funnelchat.json`) is the portal's full unified spec of
|
|
711
|
+
The spec (`spec/funnelchat.json`) is the portal's full unified spec of 06-11 (developers-portal `feat/plantillas-de-meta` @ `51c0aeb`, v0.6.7), made from accounts v1.5.97, conversations v1.19.37 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:
|
|
711
712
|
|
|
712
713
|
- `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`).
|
|
713
714
|
- `spec/pending-sandbox.json`: what the sandbox adds, with its sources inside.
|
|
@@ -750,7 +751,7 @@ What it does:
|
|
|
750
751
|
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.
|
|
751
752
|
3. It writes `generated/openapi.json`, the spec the sandbox validates and answers with.
|
|
752
753
|
|
|
753
|
-
**The current spec** is the portal's full unified spec of
|
|
754
|
+
**The current spec** is the portal's full unified spec of 06-11 (`feat/plantillas-de-meta` @ `51c0aeb`, `generated/specs/funnelchat-completa.json`): 219 paths, 266 operations and 29 events of accounts v1.5.97, conversations v1.19.37 (`a4d0136be`) and communities v2.35.7. The detail is in `spec/procedencia.json`.
|
|
754
755
|
|
|
755
756
|
## Development
|
|
756
757
|
|
|
@@ -7200,6 +7200,13 @@ var openapi_default = {
|
|
|
7200
7200
|
type: "boolean"
|
|
7201
7201
|
}
|
|
7202
7202
|
},
|
|
7203
|
+
{
|
|
7204
|
+
name: "awaitingReply",
|
|
7205
|
+
in: "query",
|
|
7206
|
+
schema: {
|
|
7207
|
+
type: "boolean"
|
|
7208
|
+
}
|
|
7209
|
+
},
|
|
7203
7210
|
{
|
|
7204
7211
|
name: "onlyWithMac",
|
|
7205
7212
|
in: "query",
|
|
@@ -13645,7 +13652,7 @@ var openapi_default = {
|
|
|
13645
13652
|
}
|
|
13646
13653
|
},
|
|
13647
13654
|
"x-fc-permission": "conversations:read",
|
|
13648
|
-
"x-fc-proposito": "\xDAsalo para listar tus plantillas de WhatsApp con su estado de aprobaci\xF3n, categor\xEDa, idioma y n\xFAmero, y elegir cu\xE1l usar en un env\xEDo.",
|
|
13655
|
+
"x-fc-proposito": "\xDAsalo para listar tus plantillas de WhatsApp con su estado de aprobaci\xF3n, categor\xEDa, idioma y n\xFAmero, y elegir cu\xE1l usar en un env\xEDo. Si una que creaste directamente en Meta no aparece, tr\xE1ela con \xABTraer tus plantillas de Meta\xBB (`POST /api/v1/devices/{device}/templates`) y vuelve a listar.",
|
|
13649
13656
|
"x-fc-domain": "conversations",
|
|
13650
13657
|
"x-fc-pending": "conversations-templates",
|
|
13651
13658
|
"x-fc-pending-response": "conversations-templates"
|
|
@@ -13873,6 +13880,159 @@ var openapi_default = {
|
|
|
13873
13880
|
"x-fc-domain": "conversations"
|
|
13874
13881
|
}
|
|
13875
13882
|
},
|
|
13883
|
+
"/api/v1/devices/{device}/templates": {
|
|
13884
|
+
post: {
|
|
13885
|
+
operationId: "messageTemplate.sync",
|
|
13886
|
+
description: "**Permiso requerido:** **Contactos y conversaciones \u2192 Editar**\n\nSincroniza con Meta las plantillas de un n\xFAmero de la API oficial de WhatsApp: trae las que se crearon o cambiaron directamente en Meta y quita las de una cuenta de WhatsApp Business que el n\xFAmero ya no usa. `{device}` es el `id` del n\xFAmero.\n\n- **As\xEDncrona:** la primera p\xE1gina (hasta 100 plantillas) se lee en el momento y las siguientes en segundo plano. Responde 202 con `status`: `completed` si ya termin\xF3, `in_progress` si sigue. Las plantillas sincronizadas se listan con `GET /templates`.\n- **Una a la vez:** mientras hay una sincronizaci\xF3n de ese n\xFAmero en curso, otra llamada responde 409 y no lanza una nueva.\n- **Errores de Meta:** si Meta rechaza la lectura (por ejemplo, porque el token del n\xFAmero venci\xF3 o la cuenta de WhatsApp Business no existe), responde 422 `meta_rejected` con el c\xF3digo de error de Meta en `meta_code` y no sincroniza nada.\n\n| HTTP | `error` | Cu\xE1ndo |\n|---|---|---|\n| 404 | \u2014 | El n\xFAmero no existe en tu cuenta o est\xE1 fuera del workspace de tu clave. |\n| 409 | `template_sync_in_progress` | Ya hay una sincronizaci\xF3n de ese n\xFAmero en curso. |\n| 422 | `number_not_connected_to_meta` | El n\xFAmero est\xE1 conectado por QR o no tiene una cuenta de WhatsApp Business o credenciales en Meta. |\n| 422 | `meta_rejected` | Meta rechaz\xF3 la lectura de las plantillas (`meta_code` trae su c\xF3digo de error). |\n| 429 | \u2014 | Demasiadas solicitudes por minuto (ver `Retry-After`). |\n| 502 | `meta_unavailable` | Meta no respondi\xF3. |",
|
|
13887
|
+
summary: "Sincronizar las plantillas de un n\xFAmero con Meta",
|
|
13888
|
+
tags: [
|
|
13889
|
+
"MessageTemplate"
|
|
13890
|
+
],
|
|
13891
|
+
parameters: [
|
|
13892
|
+
{
|
|
13893
|
+
name: "device",
|
|
13894
|
+
in: "path",
|
|
13895
|
+
required: true,
|
|
13896
|
+
schema: {
|
|
13897
|
+
type: "integer"
|
|
13898
|
+
}
|
|
13899
|
+
}
|
|
13900
|
+
],
|
|
13901
|
+
responses: {
|
|
13902
|
+
"202": {
|
|
13903
|
+
description: "Accepted",
|
|
13904
|
+
content: {
|
|
13905
|
+
"application/json": {
|
|
13906
|
+
schema: {
|
|
13907
|
+
type: "object",
|
|
13908
|
+
properties: {
|
|
13909
|
+
status: {
|
|
13910
|
+
type: "string",
|
|
13911
|
+
enum: [
|
|
13912
|
+
"in_progress",
|
|
13913
|
+
"completed"
|
|
13914
|
+
]
|
|
13915
|
+
}
|
|
13916
|
+
},
|
|
13917
|
+
required: [
|
|
13918
|
+
"status"
|
|
13919
|
+
]
|
|
13920
|
+
}
|
|
13921
|
+
}
|
|
13922
|
+
}
|
|
13923
|
+
},
|
|
13924
|
+
"401": {
|
|
13925
|
+
$ref: "#/components/responses/ConversationsAuthenticationException"
|
|
13926
|
+
},
|
|
13927
|
+
"403": {
|
|
13928
|
+
$ref: "#/components/responses/ConversationsAuthorizationException"
|
|
13929
|
+
},
|
|
13930
|
+
"404": {
|
|
13931
|
+
description: "Not Found",
|
|
13932
|
+
content: {
|
|
13933
|
+
"application/json": {
|
|
13934
|
+
schema: {
|
|
13935
|
+
type: "object",
|
|
13936
|
+
properties: {
|
|
13937
|
+
message: {
|
|
13938
|
+
type: "string"
|
|
13939
|
+
}
|
|
13940
|
+
},
|
|
13941
|
+
required: [
|
|
13942
|
+
"message"
|
|
13943
|
+
]
|
|
13944
|
+
}
|
|
13945
|
+
}
|
|
13946
|
+
}
|
|
13947
|
+
},
|
|
13948
|
+
"409": {
|
|
13949
|
+
description: "Conflict",
|
|
13950
|
+
content: {
|
|
13951
|
+
"application/json": {
|
|
13952
|
+
schema: {
|
|
13953
|
+
type: "object",
|
|
13954
|
+
properties: {
|
|
13955
|
+
error: {
|
|
13956
|
+
type: "string",
|
|
13957
|
+
enum: [
|
|
13958
|
+
"template_sync_in_progress"
|
|
13959
|
+
]
|
|
13960
|
+
},
|
|
13961
|
+
message: {
|
|
13962
|
+
type: "string"
|
|
13963
|
+
}
|
|
13964
|
+
},
|
|
13965
|
+
required: [
|
|
13966
|
+
"error",
|
|
13967
|
+
"message"
|
|
13968
|
+
]
|
|
13969
|
+
}
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13972
|
+
},
|
|
13973
|
+
"422": {
|
|
13974
|
+
description: "Unprocessable Content",
|
|
13975
|
+
content: {
|
|
13976
|
+
"application/json": {
|
|
13977
|
+
schema: {
|
|
13978
|
+
type: "object",
|
|
13979
|
+
properties: {
|
|
13980
|
+
error: {
|
|
13981
|
+
type: "string",
|
|
13982
|
+
enum: [
|
|
13983
|
+
"number_not_connected_to_meta",
|
|
13984
|
+
"meta_rejected"
|
|
13985
|
+
]
|
|
13986
|
+
},
|
|
13987
|
+
message: {
|
|
13988
|
+
type: "string"
|
|
13989
|
+
},
|
|
13990
|
+
meta_code: {
|
|
13991
|
+
type: [
|
|
13992
|
+
"integer",
|
|
13993
|
+
"null"
|
|
13994
|
+
]
|
|
13995
|
+
}
|
|
13996
|
+
},
|
|
13997
|
+
required: [
|
|
13998
|
+
"error",
|
|
13999
|
+
"message"
|
|
14000
|
+
]
|
|
14001
|
+
}
|
|
14002
|
+
}
|
|
14003
|
+
}
|
|
14004
|
+
},
|
|
14005
|
+
"502": {
|
|
14006
|
+
description: "Bad Gateway",
|
|
14007
|
+
content: {
|
|
14008
|
+
"application/json": {
|
|
14009
|
+
schema: {
|
|
14010
|
+
type: "object",
|
|
14011
|
+
properties: {
|
|
14012
|
+
error: {
|
|
14013
|
+
type: "string",
|
|
14014
|
+
enum: [
|
|
14015
|
+
"meta_unavailable"
|
|
14016
|
+
]
|
|
14017
|
+
},
|
|
14018
|
+
message: {
|
|
14019
|
+
type: "string"
|
|
14020
|
+
}
|
|
14021
|
+
},
|
|
14022
|
+
required: [
|
|
14023
|
+
"error",
|
|
14024
|
+
"message"
|
|
14025
|
+
]
|
|
14026
|
+
}
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
14029
|
+
}
|
|
14030
|
+
},
|
|
14031
|
+
"x-fc-permission": "conversations:write",
|
|
14032
|
+
"x-fc-proposito": "\xDAsalo si creaste o cambiaste una plantilla directamente en Meta y todav\xEDa no aparece en tu lista (llegan solas una vez por d\xEDa): la trae en el momento. Solo con n\xFAmeros oficiales de WhatsApp. Si responde `in_progress`, vuelve a listar en unos segundos.",
|
|
14033
|
+
"x-fc-domain": "conversations"
|
|
14034
|
+
}
|
|
14035
|
+
},
|
|
13876
14036
|
"/api/v1/devices/{device}/templates/{template}/send-template": {
|
|
13877
14037
|
post: {
|
|
13878
14038
|
operationId: "messageTemplate.send",
|
|
@@ -14190,7 +14350,7 @@ var openapi_default = {
|
|
|
14190
14350
|
}
|
|
14191
14351
|
},
|
|
14192
14352
|
"x-fc-permission": "conversations:send",
|
|
14193
|
-
"x-fc-proposito": "Sirve para enviar una plantilla desde uno de tus n\xFAmeros a un tel\xE9fono o contacto, completando sus variables, por ejemplo un recordatorio, una confirmaci\xF3n o una novedad.",
|
|
14353
|
+
"x-fc-proposito": "Sirve para enviar una plantilla desde uno de tus n\xFAmeros a un tel\xE9fono o contacto, completando sus variables, por ejemplo un recordatorio, una confirmaci\xF3n o una novedad. Si una plantilla que creaste directamente en Meta todav\xEDa no aparece en tu lista, tr\xE1ela con \xABTraer tus plantillas de Meta\xBB (`POST /api/v1/devices/{device}/templates`).",
|
|
14194
14354
|
"x-fc-domain": "conversations",
|
|
14195
14355
|
"x-fc-pending": "conversations-send",
|
|
14196
14356
|
"x-fc-pending-response": "conversations-send"
|
|
@@ -35443,7 +35603,7 @@ var openapi_default = {
|
|
|
35443
35603
|
"broadcast.paused": {
|
|
35444
35604
|
post: {
|
|
35445
35605
|
summary: "broadcast.paused",
|
|
35446
|
-
description: "Una campa\xF1a se paus\xF3 sola, por ejemplo porque baj\xF3 la calidad del n\xFAmero.\n\n`reason` es `quality_degraded` (la calidad del n\xFAmero baj\xF3 respecto de la del inicio, o lleg\xF3 a RED), `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos), `template_unavailable` (Meta dej\xF3 la plantilla en `PAUSED`, `DISABLED` o `REJECTED` antes de una tanda) o `rejection_rate` (
|
|
35606
|
+
description: "Una campa\xF1a se paus\xF3 sola, por ejemplo porque baj\xF3 la calidad del n\xFAmero.\n\n`reason` es `quality_degraded` (la calidad del n\xFAmero baj\xF3 respecto de la del inicio, o lleg\xF3 a RED), `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos), `template_unavailable` (Meta dej\xF3 la plantilla en `PAUSED`, `DISABLED` o `REJECTED` antes de una tanda), `payment_issue` (el 2 % o m\xE1s de los env\xEDos de la misma campa\xF1a, con un m\xEDnimo de 20 env\xEDos, fueron rechazados por Meta con el c\xF3digo 131042, un problema de pago o de facturaci\xF3n de la cuenta de WhatsApp Business; el 2 % y el m\xEDnimo de 20 env\xEDos son los valores por defecto y se pueden configurar; aplica a cualquier campa\xF1a, tenga o no env\xEDo inteligente; no frena el n\xFAmero; al reanudar la campa\xF1a el conteo, incluidos los env\xEDos que cuentan, vuelve a empezar de cero) o `rejection_rate` (entre los env\xEDos recientes de la propia campa\xF1a, por defecto las \xFAltimas 24 horas, Meta rechaz\xF3 una proporci\xF3n igual o mayor al umbral configurable, por defecto 15 %, con los c\xF3digos que indican una restricci\xF3n de la cuenta o del n\xFAmero; la lista es configurable y por defecto es 131048, 131031 y 368; los c\xF3digos 131049 y 131026 no pausan la campa\xF1a). Con `rejection_rate` llega adem\xE1s `rejection`: `codes` (los que llegaron a su umbral: hoy `failed`), `rates` (el porcentaje de cada uno; las tasas de `131049` y `131026` se informan como dato aunque no pausen), `attempts` (los env\xEDos medidos) y `window_hours`. `quality` y `quality_baseline` son la calidad de Meta le\xEDda en el control y la del inicio del env\xEDo (`GREEN`, `YELLOW`, `RED`), o `null` si no se pudo leer, y siempre `null` en una difusi\xF3n sin env\xEDo inteligente. `template` es `{name, status}` con el nombre de la plantilla y su estado en Meta cuando `reason` es `template_unavailable`, y `null` en las dem\xE1s pausas (por calidad, pago o rechazos). Con `rejection_rate` el n\xFAmero adem\xE1s queda frenado 7 d\xEDas (configurable): mientras dure no se pueden crear, ejecutar ni reanudar campa\xF1as desde ese n\xFAmero, ni mandar plantillas a contactos que no le escribieron en las \xFAltimas 24 horas, y solo soporte lo levanta antes. Las pausas por calidad, por plantilla y por rechazos son solo de las campa\xF1as con env\xEDo inteligente; la pausa por `payment_issue` puede ocurrir en cualquier campa\xF1a. Reanudar la campa\xF1a y que se pause otra vez emite otro evento.",
|
|
35447
35607
|
"x-fc-status": "live",
|
|
35448
35608
|
parameters: [
|
|
35449
35609
|
{
|
|
@@ -51327,9 +51487,9 @@ var openapi_default = {
|
|
|
51327
51487
|
version: "0.0.1",
|
|
51328
51488
|
spec: "specs/conversations.json",
|
|
51329
51489
|
spec_changes: "La especificaci\xF3n del \xE1rea con `servers` en la URL base p\xFAblica y, en el texto, cada permiso nombrado como en el panel (\xAB\xC1rea \u2192 Casilla\xBB), sin el nombre t\xE9cnico; el nombre t\xE9cnico queda en `x-fc-permission`. En la publicada, cada operaci\xF3n lleva el nombre y el grupo del portal.",
|
|
51330
|
-
operations:
|
|
51490
|
+
operations: 129,
|
|
51331
51491
|
excluded_routes: 20,
|
|
51332
|
-
description: 'Public API of Funnelchat Conversations. Authenticate every request with `Authorization: Bearer <id>|fc_live_...`. Each operation shows the permission your key needs, as the Developers panel names it: **Contacts and conversations \u2192 View**, **Contacts and conversations \u2192 Edit** or **Contacts and conversations \u2192 Send**.\n\n## Webhooks\n\nLos webhooks avisan a un sistema externo, por HTTP, cuando algo pasa en tu cuenta, sin que tengas que consultar la API cada tanto. Registras una URL HTTPS, eliges los tipos de evento que quieres recibir y Funnelchat env\xEDa a esa URL un `POST` firmado cada vez que ocurre uno. Las rutas de administraci\xF3n requieren una clave de API y que la use el titular de la cuenta.\n\n### C\xF3mo suscribirse\n\nEstas son las nueve rutas para administrar suscripciones:\n\n| Ruta | Para qu\xE9 sirve | Permiso |\n| --- | --- | --- |\n| `GET /api/v1/webhooks` | Listar las suscripciones | Contactos y conversaciones \u2192 Ver |\n| `POST /api/v1/webhooks` | Crear una suscripci\xF3n | Contactos y conversaciones \u2192 Editar |\n| `GET /api/v1/webhooks/events` | Ver el cat\xE1logo de tipos de evento | Contactos y conversaciones \u2192 Ver |\n| `GET /api/v1/webhooks/{webhook}` | Ver una suscripci\xF3n | Contactos y conversaciones \u2192 Ver |\n| `PUT /api/v1/webhooks/{webhook}` | Cambiar URL, tipos o estado activo | Contactos y conversaciones \u2192 Editar |\n| `DELETE /api/v1/webhooks/{webhook}` | Eliminar una suscripci\xF3n | Contactos y conversaciones \u2192 Editar |\n| `POST /api/v1/webhooks/{webhook}/rotate-secret` | Generar un secreto nuevo | Contactos y conversaciones \u2192 Editar |\n| `POST /api/v1/webhooks/{webhook}/test` | Enviar ahora un evento de prueba | Contactos y conversaciones \u2192 Editar |\n| `GET /api/v1/webhooks/{webhook}/deliveries` | Ver el historial de entregas | Contactos y conversaciones \u2192 Ver |\n\nEjemplo de alta:\n\n```bash\ncurl -X POST "https://TU-HOST/api/v1/webhooks" \\\n -H "Authorization: Bearer <id>|fc_live_..." \\\n -H "Content-Type: application/json" \\\n -d \'{\n "url": "https://example.com/hooks/funnelchat",\n "description": "Mi integraci\xF3n",\n "events": ["message.received"]\n }\'\n```\n\nRespuesta `201`:\n\n```json\n{\n "data": {\n "uuid": "00000000-0000-4000-8000-000000000001",\n "url": "https://example.com/hooks/funnelchat",\n "description": "Mi integraci\xF3n",\n "events": ["message.received"],\n "active": true,\n "inactive_reason": null,\n "created_at": "2026-10-01T12:00:00+00:00",\n "updated_at": "2026-10-01T12:00:00+00:00",\n "secret": "<secreto de firma>"\n }\n}\n```\n\nEl `secret` se muestra **una sola vez**, en esta respuesta (y al rotarlo). Gu\xE1rdalo: no se puede volver a consultar. En `events` puedes usar los tipos `message.received`, `flow.completed`, `broadcast.finished`, `contact.stage_changed`, `message.status_changed`, `conversation.closed`, `conversation.assigned`, `contact.tag_added`, `broadcast.paused`, `contact.tag_removed`, `contact.created`, `template.status_changed`, `flow.failed`, `whatsapp_form.completed` o `*` para todos los dem\xE1s que se puedan suscribir (`message.status_changed` siempre se suscribe de forma expl\xEDcita: `*` no lo incluye). La URL debe ser `https` y apuntar a un destino p\xFAblico.\n\n### El sobre\n\nCada entrega es un `POST` con un cuerpo JSON con esta forma. El contenido de `data` depende del tipo de evento (ver el cat\xE1logo m\xE1s abajo).\n\n| Campo | Descripci\xF3n |\n| --- | --- |\n| `id` | Identificador \xFAnico del evento (`evt_...`). Es el mismo en todos los reintentos. |\n| `type` | Tipo de evento, por ejemplo `message.received`. |\n| `version` | Versi\xF3n del formato del evento (hoy `1`). |\n| `occurred_at` | Momento en que ocurri\xF3, en UTC (ISO 8601). |\n| `data` | Datos del evento. |\n\n### Cabeceras\n\n| Cabecera | Contenido |\n| --- | --- |\n| `Content-Type` | `application/json` |\n| `X-Funnelchat-Signature` | Firma de la entrega, con la forma `t=<timestamp>,v1=<firma>` |\n| `X-Funnelchat-Event` | Tipo de evento (igual que `type` en el cuerpo) |\n| `X-Funnelchat-Delivery` | Identificador de esta entrega (cambia en cada intento) |\n\n### C\xF3mo verificar la firma\n\nCada entrega lleva la cabecera `X-Funnelchat-Signature` con la forma `t=<timestamp>,v1=<firma>`. Para verificarla:\n\n1. Toma el **cuerpo crudo** de la petici\xF3n, tal como lleg\xF3, sin volver a codificar el JSON.\n2. Arma el texto `t + "." + cuerpo` (el timestamp, un punto y el cuerpo).\n3. Calcula su HMAC-SHA256 con el secreto de la suscripci\xF3n y comp\xE1ralo, en tiempo constante, con `v1`.\n4. Rechaza la entrega si `t` se aleja del reloj de tu servidor m\xE1s de 300 segundos.\n\nEjemplo en PHP:\n\n```php\nfunction verificarFirma(string $cuerpoCrudo, string $cabecera, string $secreto, int $tolerancia = 300): bool\n{\n parse_str(str_replace(\',\', \'&\', $cabecera), $partes);\n\n if (! isset($partes[\'t\'], $partes[\'v1\']) || ! ctype_digit($partes[\'t\'])) {\n return false;\n }\n\n if (abs(time() - (int) $partes[\'t\']) > $tolerancia) {\n return false;\n }\n\n $esperada = hash_hmac(\'sha256\', $partes[\'t\'].\'.\'.$cuerpoCrudo, $secreto);\n\n return hash_equals($esperada, $partes[\'v1\']);\n}\n```\n\nEjemplo en JavaScript (Node.js):\n\n```javascript\nconst crypto = require(\'crypto\');\n\nfunction verificarFirma(cuerpoCrudo, cabecera, secreto, tolerancia = 300) {\n const partes = Object.fromEntries(cabecera.split(\',\').map((p) => p.split(\'=\')));\n if (!/^\\d+$/.test(partes.t || \'\') || !partes.v1) return false;\n\n if (Math.abs(Date.now() / 1000 - Number(partes.t)) > tolerancia) return false;\n\n const esperada = crypto\n .createHmac(\'sha256\', secreto)\n .update(`${partes.t}.${cuerpoCrudo}`)\n .digest(\'hex\');\n\n const a = Buffer.from(esperada);\n const b = Buffer.from(partes.v1);\n\n return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\nEn JavaScript, lee el cuerpo como texto o `Buffer` antes de convertirlo a objeto (por ejemplo con `express.raw({ type: \'application/json\' })`).\n\n### Reintentos\n\nUna entrega es exitosa si tu servidor responde con un c\xF3digo `2xx` en 10 segundos o menos. Funnelchat no sigue redirecciones.\n\n- Se hacen hasta **4 intentos** por evento y suscripci\xF3n. Entre un intento y el siguiente se espera 60, 300, 1800 segundos.\n- Se reintenta cuando hay un error de red o un timeout, ante un `5xx` y ante un `429`.\n- Un `3xx` o cualquier otro `4xx` es terminal: no se reintenta.\n- Despu\xE9s del \xFAltimo intento fallido la entrega queda como `failed`, sin m\xE1s reintentos.\n\n### Estados de entrega\n\n`GET /api/v1/webhooks/{webhook}/deliveries` devuelve un registro por intento, con estos estados:\n\n| Estado | Significado |\n| --- | --- |\n| `pending` | El intento est\xE1 en curso: todav\xEDa no hay resultado. |\n| `succeeded` | Tu servidor respondi\xF3 `2xx`. |\n| `failed` con `next_attempt_at` | El intento fall\xF3 y hay otro programado para esa fecha. |\n| `failed` sin `next_attempt_at` | El intento fall\xF3 y no habr\xE1 m\xE1s: era terminal o se agotaron los intentos. |\n\n### Probar una suscripci\xF3n\n\n`POST /api/v1/webhooks/{webhook}/test` env\xEDa ahora un evento `webhook.test` firmado a la URL de la suscripci\xF3n, sin esperar a que ocurra nada en tu cuenta, y responde con el resultado del intento. Funciona tambi\xE9n sobre una suscripci\xF3n inactiva, para probarla antes de activarla.\n\nLa respuesta es un \xEDtem del historial de entregas, con la misma forma que cada elemento de `GET /api/v1/webhooks/{webhook}/deliveries`:\n\n```json\n{\n "data": {\n "uuid": "0d3c2f4e-6a1b-4c8d-9e2f-5b7a1c3d4e6f",\n "event_id": "evt_01hexample",\n "event_type": "webhook.test",\n "attempt": 1,\n "status": "succeeded",\n "http_status": 200,\n "duration_ms": 184,\n "error": null,\n "next_attempt_at": null,\n "created_at": "2026-01-15T12:00:00+00:00",\n "updated_at": "2026-01-15T12:00:00+00:00"\n}\n}\n```\n\n`status` es `succeeded` solo si tu servidor respondi\xF3 `2xx`; si no, es `failed` y `error` indica la causa. La prueba aparece en el historial con `event_type: "webhook.test"`, no se reintenta y no consume tu cupo de eventos.\n\n### Suscripciones creadas con una clave de API\n\nUna suscripci\xF3n creada, o reactivada, con una clave de API queda atada a esa clave. Tambi\xE9n la atan a una clave cambiar su URL con esa clave, o rotar su secreto con esa clave (rotarlo desde la sesi\xF3n web no modifica el v\xEDnculo): la suscripci\xF3n queda atada a la \xFAltima credencial que fij\xF3 su destino. Si la clave se revoca (o vence, que cuenta como revocada), la suscripci\xF3n deja de recibir eventos: pasa a `active: false` con `inactive_reason: "key_revoked"`, y no se le env\xEDan m\xE1s eventos.\n\nEse cambio se ve a partir del primer evento que ocurra despu\xE9s de revocar la clave. Hasta entonces, un `GET` puede seguir mostr\xE1ndola activa, pero no recibe eventos.\n\nLa prueba manual (`POST /api/v1/webhooks/{webhook}/test`) sigue funcionando sobre una suscripci\xF3n inactiva.\n\nUna suscripci\xF3n con `inactive_reason: "key_revoked"` no se puede reactivar: `PUT /api/v1/webhooks/{webhook}` con `"active": true` responde 422, sin importar la credencial que se use. Para seguir recibiendo eventos, crea una suscripci\xF3n nueva con `POST /api/v1/webhooks`. Una suscripci\xF3n que desactivaste a mano s\xED se puede reactivar con `PUT` y `"active": true`; queda atada a la credencial que la reactiva. Mientras est\xE9 activa, `inactive_reason` es `null`.\n\nLas suscripciones cuyo destino fij\xF3 por \xFAltima vez la sesi\xF3n web no dependen de ninguna clave: revocar una clave no las afecta.\n\n### Idempotencia\n\nUn mismo evento puede llegarte m\xE1s de una vez (por ejemplo, si tu servidor proces\xF3 la entrega pero respondi\xF3 tarde). Usa el campo `id` del sobre para descartar los que ya procesaste: es el mismo en todos los reintentos del evento.\n\n### Eventos disponibles\n\nSe generan desde el cat\xE1logo vigente. Los tipos marcados como **planificado** todav\xEDa no se emiten: se anuncian para que puedas prepararte, pero no los recibir\xE1s.\n\n`webhook.test` solo se env\xEDa con `POST /api/v1/webhooks/{webhook}/test`.\n\n#### `message.received`\n\nUn contacto le escribi\xF3 a un n\xFAmero de WhatsApp de la cuenta.\n\n```json\n{\n "id": "evt_000000000000000000000001",\n "type": "message.received",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "message": {\n "id": 1001,\n "type": "text",\n "text": "Hola, quiero m\xE1s informaci\xF3n",\n "media": null\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "receiver": {\n "phone": "+5491100000001"\n },\n "device_id": 4521\n }\n}\n```\n\nValores posibles de `data.message.type`: `text`, `image`, `video`, `document`, `sticker`, `contact`, `audio`, `note`, `action`, `invalid`, `interactive`, `location`, `voice_call`, `video_call`, `event`, `button`, `template`, `notification`, `option_list`, `template_button_reply`, `poll`, `whatsapp_form`, `call_permission_request`, `call_permission_reply`, `request_contact_info`.\n\n`device_id` es el n\xFAmero de tu cuenta que recibi\xF3 el mensaje: el mismo `device_id` que mandas para responder por ese n\xFAmero. Es `null` si el mensaje no tiene n\xFAmero asociado.\n\n#### `flow.completed`\n\nUn contacto termin\xF3 un flujo de automatizaci\xF3n.\n\n```json\n{\n "id": "evt_000000000000000000000003",\n "type": "flow.completed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "flow": {\n "id": 3001,\n "name": "Flujo de ejemplo"\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n }\n }\n}\n```\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.\n\n#### `broadcast.finished`\n\nUna campa\xF1a lleg\xF3 a su estado final (enviada, fallida o con fallos parciales), con sus conteos.\n\n```json\n{\n "id": "evt_000000000000000000000011",\n "type": "broadcast.finished",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "broadcast": {\n "id": 6001,\n "name": "Campa\xF1a de ejemplo"\n },\n "status": "partially_failed",\n "reason": "completed",\n "totals": {\n "processed": 1200,\n "failed": 30,\n "skipped_unreachable": 12\n }\n }\n}\n```\n\n`status` es `success`, `failed` o `partially_failed`. `reason` dice por qu\xE9 termin\xF3: `completed` (se enviaron todas las p\xE1ginas), `cancelled` (se cerr\xF3 una campa\xF1a pausada), `template_rejected` (Meta rechaz\xF3 la plantilla, o su categor\xEDa cambi\xF3, mientras la campa\xF1a esperaba; tambi\xE9n cuando Meta la paus\xF3 durante el env\xEDo y se detuvo la campa\xF1a), `approval_expired` (la plantilla no se aprob\xF3 a tiempo), `mac_lockout` (la cuenta alcanz\xF3 su l\xEDmite de contactos activos), `mac_check_unavailable` (la campa\xF1a la inici\xF3 una API key y el l\xEDmite de contactos activos no se pudo verificar, as\xED que no se envi\xF3 nada), `no_recipients` (la audiencia qued\xF3 vac\xEDa) o `error` (el env\xEDo no pudo despacharse). `totals` son los conteos del env\xEDo: `processed` (destinatarios procesados), `failed` y `skipped_unreachable` (destinatarios que el canal no puede alcanzar); valen 0 si la campa\xF1a termin\xF3 antes de enviar. Se emite una sola vez por campa\xF1a.\n\n#### `contact.stage_changed`\n\nUn contacto cambi\xF3 de etapa en un tablero.\n\n```json\n{\n "id": "evt_000000000000000000000008",\n "type": "contact.stage_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "board": {\n "id": 5001,\n "name": "Embudo de ejemplo"\n },\n "kind": "move",\n "from_stage": {\n "id": 6001,\n "name": "Nuevo",\n "position": 0\n },\n "to_stage": {\n "id": 6002,\n "name": "En seguimiento",\n "position": 1\n }\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto que cambia de etapa en un tablero por etapas. `kind` es entry (entra al tablero, `from_stage` es null), move (pasa de una etapa a otra), exit (sale del tablero, `to_stage` es null) o stage_deleted (se reubica porque su etapa se borr\xF3; el nombre de la etapa borrada es null). No se env\xEDa si el contacto se coloca en la etapa en la que ya estaba.\n\n#### `message.status_changed`\n\nCambi\xF3 el estado de entrega de un mensaje enviado.\n\n```json\n{\n "id": "evt_000000000000000000000014",\n "type": "message.status_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "message_id": 1001,\n "contact_id": 2001,\n "device_id": 4521,\n "status": "failed",\n "previous_status": "sent",\n "error": {\n "code": 131047,\n "title": "Re-engagement message",\n "reason": null\n },\n "occurred_at": "2026-10-01T12:00:00Z",\n "client_reference": "pedido-1001"\n }\n}\n```\n\nSe emite cuando cambia el estado de entrega de un mensaje que enviaste a un contacto, ya sea desde el chat, una automatizaci\xF3n o un asistente de IA (incluye los enviados con clave). No se emite para mensajes de difusiones ni para mensajes recibidos. `message_id` es el id que devuelve la API, `contact_id` el del contacto y `device_id` el n\xFAmero de tu cuenta por el que sali\xF3 el mensaje, el mismo `device_id` que usas para enviar (`null` si no tiene). `status` es `sent`, `delivered`, `read` o `failed`. `previous_status` es el estado que el mensaje ten\xEDa justo antes de este cambio y es `null` en tres casos: es el primer aviso del mensaje, es el mismo estado que `status` (por ejemplo, el proveedor confirma el `sent` que ya registr\xF3 el env\xEDo) o no se pudo leer. `error` solo viene cuando `status` es `failed`: `code` es el c\xF3digo num\xE9rico del proveedor y `title` su t\xEDtulo, y cualquiera de los dos puede ser `null` si el proveedor no los informa. `error.reason` es `null` cuando el fallo lo inform\xF3 el proveedor; cuando Funnelchat decidi\xF3 que un mensaje enviado con clave no sal\xEDa (por ejemplo, por saldo insuficiente o ventana de 24 horas cerrada) trae el motivo (`device_unavailable`, `insufficient_balance`, `window_closed`, `recipient_unreachable`, `template_not_found`, `template_unresolved_placeholder`, `not_queued` o `not_sent`), `code` es `null` y `title` es un texto llano en el idioma de la cuenta. `occurred_at` es el momento en que Funnelchat registr\xF3 el estado, en UTC. `client_reference` es la referencia que mandaste al enviar el mensaje (por ejemplo `pedido-1001`) y `null` si el env\xEDo no la llevaba: sirve para relacionar cada aviso con tu env\xEDo tambi\xE9n cuando la llamada de env\xEDo no respondi\xF3. Cada estado se informa una sola vez por mensaje: si llegan varios avisos juntos se informa el m\xE1s avanzado y un aviso atrasado que no cambia el estado no genera evento. Un mensaje que no lleg\xF3 a salir (por ejemplo, por ventana de 24 horas cerrada) no recibe avisos de estado, salvo uno enviado con clave: ese recibe un \xFAnico aviso `failed` con `previous_status` en `null`, su `error.reason` y su `client_reference`. Un mensaje enviado con clave cuyo resultado no se conoce (el proveedor no respondi\xF3 o el env\xEDo se interrumpi\xF3) no recibe aviso. Un mensaje cuyo env\xEDo no empez\xF3 dentro de la hora ya no sale: pasada la hora sin resultado, su estado es `null`, o `failed` con `error.reason` `not_sent` y su aviso si su env\xEDo llega tarde. Reintentar es un env\xEDo nuevo, que s\xF3lo puede duplicar si el env\xEDo hab\xEDa empezado y se cort\xF3. Un mensaje `queued` no es enviado: su resultado llega por este evento o por la consulta del mensaje. En cuentas con mucho tr\xE1fico, los eventos que excedan el l\xEDmite de entrega por minuto de la cuenta se descartan. Para recibirlo debes suscribirte a `message.status_changed` de forma expl\xEDcita: una suscripci\xF3n a `*` no lo incluye.\n\n#### `conversation.closed`\n\nUna conversaci\xF3n pas\xF3 a cerrada.\n\n```json\n{\n "id": "evt_000000000000000000000004",\n "type": "conversation.closed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n }\n }\n}\n```\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.\n\n#### `conversation.assigned`\n\nUna conversaci\xF3n qued\xF3 asignada a una persona del equipo o a un agente de IA.\n\n```json\n{\n "id": "evt_000000000000000000000005",\n "type": "conversation.assigned",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "assignee": {\n "type": "human",\n "name": "Agente de ejemplo"\n }\n }\n}\n```\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.\n\n#### `contact.tag_added`\n\nUn contacto recibi\xF3 una etiqueta.\n\n```json\n{\n "id": "evt_000000000000000000000006",\n "type": "contact.tag_added",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "tag": {\n "id": 4001,\n "name": "Etiqueta de ejemplo"\n },\n "source": "manual"\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto y etiqueta agregados, tambi\xE9n en operaciones en bloque; no se env\xEDa si el contacto ya ten\xEDa la etiqueta. `source` indica el origen: manual, flow, bulk, import, sync, walink o ai. En operaciones en bloque grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `broadcast.paused`\n\nUna campa\xF1a se paus\xF3 sola, por ejemplo porque baj\xF3 la calidad del n\xFAmero.\n\n```json\n{\n "id": "evt_000000000000000000000007",\n "type": "broadcast.paused",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "broadcast": {\n "id": 6001,\n "name": "Campa\xF1a de ejemplo"\n },\n "reason": "quality_degraded",\n "quality": "YELLOW",\n "quality_baseline": "GREEN",\n "template": null\n }\n}\n```\n\n`reason` es `quality_degraded` (la calidad del n\xFAmero baj\xF3 respecto de la del inicio, o lleg\xF3 a RED), `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos), `template_unavailable` (Meta dej\xF3 la plantilla en `PAUSED`, `DISABLED` o `REJECTED` antes de una tanda) o `rejection_rate` (en los env\xEDos recientes del n\xFAmero, por defecto las \xFAltimas 24 horas, Meta rechaz\xF3 una proporci\xF3n igual o mayor al umbral configurable: el c\xF3digo 131049, por defecto 5 %; el c\xF3digo 131026, por defecto 5 %; o los c\xF3digos que indican una restricci\xF3n de la cuenta o del n\xFAmero, por defecto 15 %, con la lista configurable y por defecto 131049, 131048, 131031 y 368). Con `rejection_rate` llega adem\xE1s `rejection`: `codes` (los que llegaron a su umbral: `131049`, `131026` o `failed`), `rates` (el porcentaje de cada uno), `attempts` (los env\xEDos medidos) y `window_hours`. `quality` y `quality_baseline` son la calidad de Meta le\xEDda en el control y la del inicio del env\xEDo (`GREEN`, `YELLOW`, `RED`), o `null` si no se pudo leer. `template` es `{name, status}` con el nombre de la plantilla y su estado en Meta cuando `reason` es `template_unavailable`, y `null` en las dem\xE1s pausas (por calidad o por rechazos). Con `rejection_rate` el n\xFAmero adem\xE1s queda frenado 7 d\xEDas (configurable): mientras dure no se pueden crear, ejecutar ni reanudar campa\xF1as desde ese n\xFAmero, ni mandar plantillas a contactos que no le escribieron en las \xFAltimas 24 horas, y solo soporte lo levanta antes. S\xF3lo las campa\xF1as con env\xEDo inteligente se pausan solas. Reanudar la campa\xF1a y que se pause otra vez emite otro evento.\n\n#### `contact.tag_removed`\n\nUn contacto perdi\xF3 una etiqueta.\n\n```json\n{\n "id": "evt_000000000000000000000009",\n "type": "contact.tag_removed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "tag": {\n "id": 4001,\n "name": "Etiqueta de ejemplo"\n },\n "source": "manual"\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto y etiqueta quitados, tambi\xE9n en operaciones en bloque; no se env\xEDa si el contacto no ten\xEDa la etiqueta. No se env\xEDa al borrar la etiqueta, al borrar o fusionar contactos ni al desvincular etiquetas de un tablero. `source` indica el origen: manual, flow, bulk o ai. En operaciones en bloque grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `contact.created`\n\nSe cre\xF3 un contacto nuevo en la cuenta.\n\n```json\n{\n "id": "evt_000000000000000000000010",\n "type": "contact.created",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000",\n "email": "contacto@example.com"\n }\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto nuevo, sea cual sea el canal: un mensaje entrante, la creaci\xF3n manual, una importaci\xF3n o la API. No se env\xEDa cuando el contacto ya exist\xEDa y s\xF3lo se actualiza, ni para los contactos que la plataforma crea para sus propias notificaciones. En importaciones grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `template.status_changed`\n\nUna plantilla cambi\xF3 de estado de aprobaci\xF3n en Meta.\n\n```json\n{\n "id": "evt_000000000000000000000012",\n "type": "template.status_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "template": {\n "id": 7001,\n "uid": "1234567890",\n "name": "plantilla_ejemplo",\n "category": "MARKETING"\n },\n "previous_status": "PENDING",\n "status": "APPROVED",\n "reason": null\n }\n}\n```\n\n`template` identifica la plantilla igual que `GET /templates` (`id`, `uid`, `name`, `category`). `previous_status` y `status` son los estados tal como los devuelve la API (por ejemplo `PENDING`, `APPROVED`, `REJECTED`, `PAUSED`); `previous_status` es `null` si la plantilla no ten\xEDa estado. `reason` trae la observaci\xF3n de rechazo (traducida como en la API y limitada a 500 caracteres) cuando el cambio es un rechazo informado al crear la plantilla, y es `null` en el resto. No se emite si Meta reenv\xEDa el estado que la plantilla ya ten\xEDa. Si una misma plantilla de Meta figura en varias cuentas, cada cuenta recibe su propio evento.\n\n#### `flow.failed`\n\nLa ejecuci\xF3n de un flujo para un contacto termin\xF3 por un error.\n\n```json\n{\n "id": "evt_000000000000000000000013",\n "type": "flow.failed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "flow": {\n "id": 3001,\n "name": "Flujo de ejemplo"\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "reason": "execution_error"\n }\n}\n```\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.\n\n#### `whatsapp_form.completed`\n\nUn contacto complet\xF3 un formulario de WhatsApp.\n\n```json\n{\n "id": "evt_000000000000000000000015",\n "type": "whatsapp_form.completed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "form": {\n "id": 4001,\n "name": "Captar un lead",\n "version_id": 4002\n },\n "message_id": 1001,\n "contact_id": 2001,\n "device_id": 5001,\n "answers": [\n {\n "key": "nombre",\n "label": "Nombre",\n "value": "Contacto de ejemplo"\n },\n {\n "key": "intereses",\n "label": "Intereses",\n "value": [\n "op_a",\n "op_b"\n ]\n },\n {\n "key": "comentarios",\n "label": "Comentarios",\n "value": null\n }\n ],\n "occurred_at": "2026-10-01T12:00:00Z"\n }\n}\n```\n\nSe env\xEDa cuando un contacto completa un formulario, aunque la respuesta llegue tarde. Puede llegar m\xE1s de una vez con el mismo `id`: deduplica por `id`. Si el contacto vuelve a enviar el mismo formulario, no se genera otro aviso: queda la primera respuesta. Cada respuesta de texto llega con a lo sumo 600 caracteres. `form.id` es el formulario (el mismo `family_id` de la lectura de formularios) y `form.version_id` la versi\xF3n que recibi\xF3 el contacto. `answers` es una lista con una entrada por pregunta, en el orden del formulario: `key` es el nombre de la pregunta, `label` su etiqueta y `value` la respuesta (texto, n\xFAmero, `true` o `false`, una lista de textos en opci\xF3n m\xFAltiple, o `null` si no la contest\xF3). En las preguntas de lista y de opci\xF3n m\xFAltiple, `value` es el id de la opci\xF3n. `message_id` es el id del mensaje del formulario y es `null` si el env\xEDo no tuvo un mensaje p\xFAblico; `device_id` es el n\xFAmero de WhatsApp por el que sali\xF3, o `null` si ya no es de la cuenta. `contact_id` es `null` si el contacto se elimin\xF3 antes de que saliera el aviso (las respuestas llegan igual); si se fusion\xF3 con otro, es el id del contacto que qued\xF3. Mientras se fusionan dos contactos, el id puede ser el del contacto que se est\xE1 fusionando.\n\n#### `webhook.test`\n\nEvento de prueba enviado desde la ruta de prueba de una suscripci\xF3n.\n\n```json\n{\n "id": "evt_000000000000000000000002",\n "type": "webhook.test",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "subscription": "00000000-0000-4000-8000-000000000001",\n "sent_at": "2026-10-01T12:00:00Z"\n }\n}\n```\n\n### Otros webhooks de la plataforma\n\nLos webhooks de alertas y el nodo \xABPetici\xF3n HTTP\xBB de las automatizaciones son otros productos, con otro formato y otra configuraci\xF3n. No se administran con las rutas de esta secci\xF3n ni llevan este sobre ni esta firma.\n\n## Listados\n\nCon una clave de API, los listados siempre responden una p\xE1gina con la forma `{ "data": [...], "links": {...}, "meta": {...} }`: `data` trae los \xEDtems de la p\xE1gina, `links` las URL de `first`, `last`, `prev` y `next` (`null` cuando no hay p\xE1gina), y `meta` el detalle (`current_page`, `from`, `last_page`, `links`, `path`, `per_page`, `to`, `total`).\n\n- **Tama\xF1o de p\xE1gina:** el par\xE1metro `perPage` es un entero entre 1 y 100. Si no lo mandas, la p\xE1gina trae 25 \xEDtems. Un valor mayor que 100, menor que 1 o que no sea un entero responde 422 con el detalle en `errors.perPage`. Algunos listados aceptan adem\xE1s `per_page` como alias con los mismos l\xEDmites; si mandas `perPage` y `per_page` con valores distintos, la respuesta es 422 con el detalle en `errors.perPage`.\n- **Orden:** cada listado documenta los par\xE1metros de orden que admite y los valores permitidos de cada uno (por ejemplo `orderBy` y `orderByMethod`). Un par\xE1metro de orden que el listado no admite, o un valor fuera de la lista, responde 422 con el detalle en `errors.<par\xE1metro>`.\n- **P\xE1ginas siguientes:** las URL de `links` y de `meta.links` conservan `perPage`, el orden y los filtros que admite el listado; los dem\xE1s par\xE1metros no se copian y s\xF3lo cambia `page`. En los listados que se piden por `POST`, los filtros viajan en el cuerpo y las URL llevan s\xF3lo `perPage` y `page`.\n- **Primero el permiso:** si tu clave no tiene el permiso del listado, la respuesta es la de siempre (401 o 403), aunque mandes un `perPage` inv\xE1lido.\n\n## Env\xEDo de mensajes con clave\n\nCuando env\xEDas con una clave de API, antes de enviar se aplican reglas de protecci\xF3n de tu n\xFAmero y de tus contactos. Si una regla frena el env\xEDo, la respuesta es un JSON `{ "error": "<c\xF3digo>", "message": "<explicaci\xF3n>" }` y no se env\xEDa nada ni se registra ning\xFAn mensaje. Cada operaci\xF3n de env\xEDo documenta los c\xF3digos que puede responder.\n- **A qui\xE9n env\xEDas con `POST /message/send-message` y `POST /devices/{device}/templates/{template_id}/send-template`:** Indica a qui\xE9n env\xEDas con `phone`: el tel\xE9fono con c\xF3digo de pa\xEDs, por ejemplo `5491122334455`. No hace falta crear el contacto antes: si ese tel\xE9fono todav\xEDa no es un contacto de tu cuenta, se crea en el workspace de tu clave, igual que con `POST /contacts/update-or-create` y con el mismo permiso, que **Enviar** ya incluye. La respuesta trae su id en `message.contact_id`, el mismo que llevan los avisos `message.status_changed`. Si el tel\xE9fono no es v\xE1lido, o ya es un contacto de otro workspace de tu cuenta, la respuesta es la misma que dar\xEDa `POST /contacts/update-or-create`: 422, o 404 sin datos de ese contacto. Con un n\xFAmero oficial de WhatsApp (API Cloud) tambi\xE9n puedes indicar `whatsapp_id`, el BSUID que env\xEDa Meta (por ejemplo `US.13491208655302741918`), en lugar de `phone`; usado as\xED con un n\xFAmero conectado por QR, responde 422. En un n\xFAmero oficial, el primer mensaje a un contacto nuevo tiene que ser una plantilla: un mensaje libre solo sale dentro de las 24 horas desde que el contacto te escribi\xF3. Si ya tienes el `contact_id`, puedes seguir envi\xE1ndolo: si junto con \xE9l mandas `phone` o `whatsapp_id`, tienen que ser de ese mismo contacto (si no, 422 `recipient_mismatch`). En `send-template`, `phone` o `whatsapp_id` va siempre, con o sin `contact_id`.\n- **Ritmo:** el l\xEDmite depende s\xF3lo del tipo de n\xFAmero por el que env\xEDas; en los env\xEDos no hay un l\xEDmite por minuto por clave ni por cuenta. Al superarlo, la respuesta es 429 con `error` = `rate_limited` y el encabezado `Retry-After`, que trae los segundos que debes esperar antes de reintentar. Las reacciones s\xF3lo tienen esta regla.\n - **N\xFAmero de WhatsApp Cloud (API oficial):** hasta 60 mensajes por segundo y hasta 10 mensajes por minuto al mismo contacto.\n - **N\xFAmero en coexistencia** (el mismo tel\xE9fono conectado a WhatsApp Cloud y a la aplicaci\xF3n WhatsApp Business del celular): hasta 15 mensajes por segundo en total entre las dos conexiones, env\xEDes por la que env\xEDes. El l\xEDmite de 10 mensajes por minuto al mismo contacto aplica s\xF3lo a lo que env\xEDas por WhatsApp Cloud.\n - **N\xFAmero conectado por c\xF3digo QR** (sin coexistencia): esta regla no limita sus env\xEDos.\n- **Ventana de 24 horas:** por un n\xFAmero de WhatsApp Cloud s\xF3lo puedes enviar mensajes que no son plantilla a un contacto que te escribi\xF3 en las \xFAltimas 24 horas. Fuera de esa ventana la respuesta es 422 con `error` = `window_closed`; para retomar la conversaci\xF3n env\xEDa una plantilla aprobada, que no tiene esta restricci\xF3n.\n- **Antibloqueo:** en las \xFAltimas 24 horas, un mismo contenido se puede enviar como m\xE1ximo 2 veces al mismo contacto (el siguiente responde 422 con `error` = `antiblock_duplicate_content`), y como m\xE1ximo 5 mensajes seguidos sin que el contacto responda (el siguiente responde 422 con `error` = `antiblock_unanswered_limit`). Cuando el contacto responde, los contadores se reinician. Aplica a mensajes y a plantillas, no a reacciones.\n- **Destinatario:** `recipient_mismatch` (422) indica que el tel\xE9fono o el identificador de WhatsApp que mandaste en el cuerpo no corresponde al contacto de la ruta (s\xF3lo las operaciones que reciben ese dato en el cuerpo). `recipient_unsupported` (422) indica que el contacto es un grupo, una lista de difusi\xF3n o un canal: por la API s\xF3lo se escribe a personas individuales.\n- **Estados de entrega:** el evento de webhook `message.status_changed` (enviado, entregado, le\xEDdo, fallido) no se incluye al suscribirte a `*`: tienes que pedirlo de forma expl\xEDcita en `events` al crear o editar la suscripci\xF3n.\n- **Resultado de tus env\xEDos:** cuando env\xEDas con una clave, la llamada no espera al proveedor: la respuesta trae el mensaje ya registrado y en cola, el mismo objeto que devuelve `GET /contacts/{contact}/messages/{message}`, y el env\xEDo sale enseguida despu\xE9s.\n - **En cola no es enviado:** `status` `queued` solo dice que registramos el mensaje y que est\xE1 por salir. El resultado llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`. Un mensaje `queued` no se reintenta: reenviarlo lo duplicar\xEDa.\n - **Aceptado no es entregado:** `status` `sent` solo dice que el proveedor acept\xF3 el mensaje. El estado final (entregado, le\xEDdo o fallido) llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`.\n - **`failed`:** el proveedor rechaz\xF3 el mensaje dando un motivo, o Funnelchat lo fren\xF3 antes de enviarlo. No sali\xF3: reintentar es un intento nuevo y no lo duplica. Cuando lo decidi\xF3 Funnelchat, `error.reason` trae el motivo, `error.code` es `null` y `error.title` es un texto llano en tu idioma; si lo rechaz\xF3 el proveedor, `error.reason` es `null`. Qu\xE9 hacer seg\xFAn `error.reason`: `device_unavailable`, reconecta el n\xFAmero en Funnelchat y env\xEDa de nuevo; `insufficient_balance`, recarga tu billetera de WhatsApp; `window_closed`, env\xEDa una plantilla aprobada; `recipient_unreachable`, revisa el tel\xE9fono del contacto; `template_not_found` o `template_unresolved_placeholder`, corrige la plantilla o sus variables; `not_queued` o `not_sent`, vuelve a intentarlo. Un mensaje enviado con clave que termina `failed` tambi\xE9n llega por `message.status_changed`, con `previous_status` en `null` y su `client_reference`; un `status` `null` no recibe aviso.\n - **`status` `null`:** el proveedor no confirm\xF3 ni rechaz\xF3 el env\xEDo (no respondi\xF3, respondi\xF3 con una falla sin motivo o el env\xEDo se interrumpi\xF3). Puede haber llegado al contacto, no recibir\xE1 avisos de estado y reintentarlo puede duplicarlo.\n - **M\xE1s de una hora en `queued`:** un mensaje cuyo env\xEDo no empez\xF3 dentro de la hora ya no sale. Pasada la hora sin resultado, su `status` es `null`, o `failed` con `error.reason` `not_sent` y su aviso `message.status_changed` si su env\xEDo llega tarde. Reintentar es un env\xEDo nuevo, que s\xF3lo puede duplicar si el env\xEDo hab\xEDa empezado y se cort\xF3.\n - **Error 5xx o llamada sin respuesta:** busca tu `client_reference` en el historial del contacto antes de reintentar, y hazlo al menos un minuto despu\xE9s de la llamada. Si aparece, el mensaje qued\xF3 registrado: no lo reenv\xEDes, su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`; la excepci\xF3n son las tarjetas y los destinos de un reenv\xEDo tras un 5xx: si todos los que aparecen vienen `failed` con `not_queued`, se reintenta la llamada completa; si alguno aparece `queued`, qued\xF3 registrado: no lo reenv\xEDes. Si no aparece, no sali\xF3 nada y puedes reintentar. Repetir una referencia no evita duplicados: dos llamadas con la misma `client_reference` registran dos mensajes.\n - **Error 404 en un env\xEDo:** el contacto no existe. En un reenv\xEDo, tambi\xE9n cuando el mensaje de origen no existe o no es de ese contacto. Al enviar por `phone` o `whatsapp_id`, cuando ese contacto es de otro workspace de tu cuenta.\n - **`POST /contacts/{contact}/message`:** la respuesta trae el mensaje en `message`: `queued` si es un mensaje, o el mensaje registrado de una nota.\n - **`POST /contacts/{contact}/message/contact-card`:** la respuesta trae `messages`: un mensaje por tarjeta, en el orden de `contact_card_ids`, cada uno con su `id` y en cola (`queued`). Todas salen despu\xE9s, desde la cola: el resultado de cada una llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Si una tarjeta no pudo encolarse, su entrada viene `failed` con `error.reason` `not_queued`: no sali\xF3 y volver a enviar esa tarjeta es un intento nuevo. Si alguna no pudo registrarse, la llamada responde un error 5xx y ninguna tarjeta sali\xF3: las que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todas las que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguna aparece `queued`, qued\xF3 registrada: no la reenv\xEDes.\n - **Reenv\xEDo:** la respuesta trae un elemento por destino, en el orden de `targets`, y cada destino que qued\xF3 en cola trae su `message` con su `id` y en cola (`queued`). Todos salen despu\xE9s, desde la cola: el resultado de cada uno llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Un destino que no pudo encolarse viene `blocked` con `reason` `not_sent` y su `message` `failed` con `error.reason` `not_queued`: no sali\xF3 y volver a reenviarlo a ese destino es un intento nuevo. Un destino `blocked` por MAC (`mac_limit_reached` o `mac_lockout_24h`) o con `reason` `target_not_found` (no existe o no es de tu cuenta), `contact_blocked` (bloqueaste al contacto en ese n\xFAmero) o `device_unavailable` trae `message` `null`: no sali\xF3 nada. Si alg\xFAn destino no pudo registrarse, la llamada responde un error 5xx y ninguno sali\xF3: los que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todos los que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguno aparece `queued`, qued\xF3 registrado: no lo reenv\xEDes.\n - **`POST /contacts/{contact}/message/form`:** la respuesta trae solo `message`: el mensaje del formulario en cola (`queued`). Si no pudo ponerse en cola, la respuesta sigue siendo 200 con el mensaje en estado `failed` y `error.reason` `not_queued`: no sali\xF3, as\xED que reintentar no lo duplica. Su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, y la respuesta del contacto por `whatsapp_form.completed`. Si tu cuenta est\xE1 en bloqueo por el l\xEDmite de contactos activos responde 429 `mac_lockout_24h` y no guarda ning\xFAn mensaje ni `client_reference`. Sus errores propios est\xE1n en la gu\xEDa \xABFormularios de WhatsApp\xBB.\n - **`POST /message/send-message` y `POST /devices/{device}/templates/{template_id}/send-template`:** la respuesta trae solo `message`: el mensaje en cola (`queued`), o el mensaje registrado de una nota en `send-message`. Si una variable de la plantilla se completa con un campo personalizado que no existe en tu cuenta, `send-template` responde 422 con `error` `template_unresolved_placeholder`, `placeholders` (hasta 20) y `count` (el total), y no registra ni env\xEDa nada; si la plantilla no existe en tu cuenta, responde 404 `template_not_found`.\n- **Correlacionar tus env\xEDos:** manda `client_reference` en el cuerpo (texto opcional de 1 a 64 caracteres visibles, sin espacios) para identificar cada env\xEDo con tu propia referencia. La misma referencia vuelve en el historial de mensajes del contacto y en el evento `message.status_changed`, de modo que puedes relacionar cada aviso de estado con tu env\xEDo tambi\xE9n cuando la llamada de env\xEDo no respondi\xF3. La referencia no es \xFAnica (dos env\xEDos pueden usar el mismo valor) y no est\xE1 autenticada: la ven todas las claves y todas las suscripciones de la cuenta. Por eso relaci\xF3nala siempre junto con `message_id` y `contact_id`, y tr\xE1tala como texto no confiable cuando la muestres. No pongas datos personales en la referencia. Una llamada que frena una regla de protecci\xF3n de esta secci\xF3n (ritmo, ventana de 24 horas, antibloqueo o destinatario) no registra ning\xFAn mensaje ni referencia. Un rechazo por MAC (`mac_limit_reached` o `mac_lockout_24h`) es distinto: guarda un mensaje con estado de error que lleva la referencia; el env\xEDo de formularios (`POST /contacts/{contact}/message/form`) es la excepci\xF3n: con `mac_lockout_24h` responde 429 y no guarda ning\xFAn mensaje ni referencia. Un mensaje enviado con clave que Funnelchat frena antes de que salga (por ejemplo, por saldo insuficiente) aparece en el historial con estado de error y su referencia, y recibe un aviso `failed` con esa referencia; uno enviado desde el panel no recibe avisos de estado. Las reacciones no aceptan `client_reference`.\n\n## Le\xEDdo y escribiendo\n\nPuedes marcar como le\xEDdo un mensaje que te mand\xF3 un contacto y mostrarle \xABescribiendo\u2026\xBB mientras preparas la respuesta. Ninguna de las dos operaciones env\xEDa un mensaje: no cuentan para el ritmo de env\xEDo ni para las dem\xE1s reglas de \xABEnv\xEDo de mensajes con clave\xBB, y requieren el permiso **Contactos y conversaciones \u2192 Enviar** porque el contacto ve su efecto.\n\n- **Tipos de n\xFAmero:** \xABmarcar como le\xEDdo\xBB funciona en n\xFAmeros de WhatsApp Cloud (API oficial, tambi\xE9n el lado oficial de un n\xFAmero en coexistencia) y en n\xFAmeros conectados por c\xF3digo QR que lo permiten; \xABescribiendo\u2026\xBB funciona solo en n\xFAmeros de WhatsApp Cloud. Si el n\xFAmero no lo permite, la respuesta es 422 con `error` = `provider_unsupported`.\n- **Marcar como le\xEDdo** (`POST /contacts/{contact}/messages/{message}/read`): el contacto ve el doble check azul. WhatsApp marca tambi\xE9n los mensajes anteriores de esa conversaci\xF3n y solo acepta mensajes recibidos en los \xFAltimos 30 d\xEDas. En Funnelchat la conversaci\xF3n queda le\xEDda, igual que cuando un agente la abre en el panel; si llegaron mensajes despu\xE9s del que marcaste, siguen contando como no le\xEDdos.\n- **\xABEscribiendo\u2026\xBB** (`POST /contacts/{contact}/typing`): se muestra sobre el \xFAltimo mensaje que el contacto te mand\xF3 en las \xFAltimas 24 horas, por el n\xFAmero que indiques en `device_id` o, si no lo mandas, por el n\xFAmero de su \xFAltimo mensaje. Dura hasta que respondes o 25 segundos, lo que pase primero. WhatsApp tambi\xE9n marca ese mensaje (y los anteriores) como le\xEDdo, as\xED que el contacto ve el doble check azul; en Funnelchat la conversaci\xF3n no cambia. P\xEDdelo solo cuando vayas a responder.\n- **L\xEDmites:** entre las dos operaciones, hasta 20 llamadas por segundo por n\xFAmero y 50 por cuenta. El l\xEDmite por cuenta incluye las llamadas rechazadas con 429 o con alguno de los c\xF3digos 422 de esta secci\xF3n; no cuentan los 404 ni los errores de validaci\xF3n del cuerpo (422 con errors). Al superar cualquiera de los dos, la respuesta es 429 con `error` = `rate_limited` y el encabezado `Retry-After`.\n- **Si WhatsApp no confirma:** la respuesta es 502 con `error` = `provider_error` y no se cambia nada; puedes reintentar.\n\n## Enviar un archivo por URL\nNo subes el archivo a Funnelchat: en `file` mandas la direcci\xF3n (URL) del archivo y WhatsApp o el proveedor lo descarga desde ah\xED. Funnelchat no abre la URL al recibir tu llamada, as\xED que un archivo inaccesible, vencido o fuera de los l\xEDmites se conoce al enviarlo: el mensaje termina `failed` con el error del proveedor.\n- **La URL:** `file` admite hasta 4096 caracteres; m\xE1s de eso se rechaza con un 422 en `file`. Si `file` empieza con `http`, en may\xFAsculas o min\xFAsculas, tiene que ser una URL que empiece con `http://` o `https://`; si no (por ejemplo `httpfoto.jpg`), la respuesta es 422 con el error en `file`. Te recomendamos `https://`; `http://` tambi\xE9n se acepta, pero viaja sin cifrar. Debe poder descargarse sin credenciales; puede ser una URL firmada. Tiene que seguir v\xE1lida hasta que el mensaje sale: un env\xEDo con clave puede esperar en cola hasta 54 minutos, as\xED que usa URLs que duren al menos una hora. No pongas secretos en la ruta ni en el dominio de la URL: en los registros de actividad (logs) del env\xEDo que escribe Funnelchat, de la URL quedan s\xF3lo el esquema, el dominio y la ruta (sin el usuario y la contrase\xF1a, la parte de consulta despu\xE9s de `?` ni el fragmento despu\xE9s de `#`) y del resultado s\xF3lo identificadores, el tipo del mensaje y, del error del proveedor, sus c\xF3digos y, s\xF3lo si tu n\xFAmero es de WhatsApp Cloud, su categor\xEDa, su t\xEDtulo corto y su identificador de traza, nunca el texto de la respuesta del proveedor; la ruta y el dominio s\xED quedan. La URL completa viaja a WhatsApp o al proveedor de tu n\xFAmero, que la descarga y puede guardarla en sus propios registros. Adem\xE1s, la URL completa, con su consulta, se guarda en el historial del contacto, la ven los usuarios de tu cuenta y se copia al reenviar el mensaje: usa URLs firmadas de corta duraci\xF3n (al menos el tiempo de la cola, cerca de una hora) y limitadas a un solo archivo.\n- **El tipo:** decide la extensi\xF3n de la ruta de la URL si es una de la lista; si no, el final de la URL completa si es una de la lista. Si la ruta no termina en una extensi\xF3n de la lista y s\xF3lo lo hace el final de la URL completa (por ejemplo `https://cdn.example.com/video.mov?poster=frame.jpg`), el mensaje queda registrado con ese tipo, pero WhatsApp o el proveedor pueden enviarlo seg\xFAn la extensi\xF3n de la ruta; si necesitas un tipo fijo, env\xEDa `file_type`:\n - `.jpg`, `.jpeg`, `.png`: `image`\n - `.webp`: `sticker`\n - `.ogg`, `.oga`, `.mp3`, `.m4a`, `.aac`, `.opus`, `.wav`: `audio`\n - `.gif`, `.mp4`: `video`\n - `.doc`, `.docx`, `.pdf`: `document`\n- **Cu\xE1ndo env\xEDas `file_type`:** si la URL no termina en una extensi\xF3n reconocida (por ejemplo una URL firmada sin extensi\xF3n), la respuesta es 422 con el error en `file_type`: agrega `file_type` con uno de estos valores: `image`, `video`, `audio`, `document`, `sticker`. Cuando lo env\xEDas, manda sobre la extensi\xF3n: sirve tambi\xE9n para un archivo cuya extensi\xF3n no es de los tipos de la lista, como `.xlsx`, que se env\xEDa con `file_type` = `document` y `file_name` (por ejemplo `reporte.xlsx`), porque un documento cuya URL no termina en una extensi\xF3n de la lista necesita su nombre. Si la URL termina en una extensi\xF3n que no es la del archivo (como `/descarga.php?id=7`) y no env\xEDas `file_type`, la respuesta es 415 con la extensi\xF3n recibida (`php`); con `file_type` se env\xEDa y, si es un documento, tambi\xE9n hace falta `file_name`.\n- **Extensi\xF3n no soportada:** si la ruta de la URL termina en un formato real que no enviamos solos (por ejemplo `.xlsx` o `.zip`), la URL completa no termina en una extensi\xF3n de la lista y no mandas `file_type`, la respuesta es 415 con la extensi\xF3n recibida.\n- **El nombre del archivo:** `file_name` (opcional, hasta 255 caracteres, sin `/`, `\\`, `?`, `#` ni caracteres de control) es el nombre con el que el contacto ve un documento; ll\xE9valo siempre con su extensi\xF3n, por ejemplo `factura.pdf`. Es obligatorio cuando env\xEDas un documento por URL y la URL no termina en una extensi\xF3n de la lista: sin \xE9l la respuesta es 422 con el error en `file_name`.\n- **Formatos y tama\xF1os:** los formatos aceptados por WhatsApp y el tama\xF1o m\xE1ximo de cada tipo son:\n - `image`: image/jpeg, image/png, image/webp; hasta 5 MB.\n - `video`: video/mp4, video/3gpp, image/gif; hasta 16 MB.\n - `audio`: audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, audio/opus; hasta 16 MB.\n - `document`: text/plain, application/pdf, application/vnd.ms-powerpoint, application/msword, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; hasta 100 MB.\n - `sticker`: image/webp; hasta 100 KB si es est\xE1tico y 500 KB si es animado.\n- **Historial y reenv\xEDo:** el historial del contacto guarda la URL tal cual la enviaste, y el nombre `file_name` no se guarda. Al reenviar un mensaje cuyo archivo es una URL, se reenv\xEDa la misma URL; con una extensi\xF3n de la lista, como esa extensi\xF3n indica; si no, un mensaje que enviaste se reenv\xEDa con su tipo s\xF3lo cuando su extensi\xF3n es un formato que WhatsApp acepta para ese tipo (los de \xABFormatos y tama\xF1os\xBB, por ejemplo `.3gp` para `video` o `.amr` para `audio`). Una URL sin extensi\xF3n, un formato no aceptado, un documento cuya URL no termina en una extensi\xF3n de la lista (por ejemplo `/descarga.php?id=7`) o un mensaje recibido sin una extensi\xF3n de la lista no se puede reenviar: la respuesta es 422 con `error` = `not_forwardable` y `reason` = `unsupported_file_extension`.\n\n## Formularios de WhatsApp\n\nUn formulario de WhatsApp es un conjunto de pantallas con preguntas que tu contacto abre y completa dentro del chat. Con la API puedes armarlo, publicarlo, enviarlo a un contacto y recibir sus respuestas. S\xF3lo funciona con n\xFAmeros de WhatsApp Cloud (API oficial) de tu cuenta, y s\xF3lo se puede enviar dentro de las 24 horas siguientes al \xFAltimo mensaje del contacto a ese n\xFAmero. Los n\xFAmeros conectados por c\xF3digo QR no sirven para formularios.\n- **Identificadores:** un formulario tiene un identificador estable y una versi\xF3n por cada vez que lo cambias. No los confundas:\n\n | Qu\xE9 es | D\xF3nde aparece |\n |---|---|\n | **Formulario** (estable, el mismo en todas sus versiones) | `family_id` en las respuestas de formularios, `{familyId}` en las rutas `/forms/families/{familyId}/\u2026`, `form_id` al enviar y `data.form.id` en `whatsapp_form.completed` |\n | **Versi\xF3n** (cambia con cada versi\xF3n nueva) | `id` en las respuestas de formularios, `{id}` en las rutas `/forms/{id}/\u2026` de edici\xF3n, publicaci\xF3n y n\xFAmeros, y `data.form.version_id` en `whatsapp_form.completed` |\n\n Al enviar pides el formulario, no la versi\xF3n: sale la versi\xF3n publicada vigente. Si publicas una versi\xF3n nueva, lo que se env\xEDa cambia sin que cambies tu integraci\xF3n.\n- **Recorrido, paso a paso:**\n 1. **Crea el formulario:** `POST /forms` con `template_key` `blank` (un formulario para armar desde cero; `GET /forms/templates` lista las dem\xE1s plantillas), `device_id` (el n\xFAmero de WhatsApp Cloud de tu cuenta) y, si quieres, `device_ids` con otros n\xFAmeros. Responde `201` con la versi\xF3n en borrador (`id`), su `family_id`, `lint` y `numbers`. Un formulario no se crea mandando su contenido: el contenido va en el paso siguiente. Ojo con `blank`: no viene vac\xEDo, trae una pantalla (`SCREEN_BLNK`) sin elementos cuyo `next` es `complete`, es decir, que termina el formulario. Si en el `PUT` conservas esa pantalla y agregas otra, la segunda queda inalcanzable y `lint` marca `screen_unreachable` (error que impide publicar) hasta que el `next` de la primera apunte a la segunda (o `auto`, que sigue el orden). Lo m\xE1s simple es reemplazar `screens` entera con tus propias pantallas.\n 2. **Arma la definici\xF3n:** `PUT /forms/{id}` con `definition` (`screens`, cada una con `id`, `title`, `cta`, `next` e `items`) y, si quieres, `name` y `expected_updated_at`. El servidor guarda la definici\xF3n tal como llega y la respuesta trae `lint` junto a `data`: una lista de `{code, severity, screenId, itemId, message}` con lo que falta corregir. Lo incompleto se guarda igual; los errores (`severity` `error`) impiden publicar y los avisos (`aviso`) no. Una definici\xF3n con la forma rota responde `422`. Para leerla de nuevo, `GET /forms/{id}` trae la versi\xF3n con su definici\xF3n y `GET /forms/families/{familyId}/definition` la del formulario: la de la versi\xF3n publicada vigente o, si todav\xEDa no hay ninguna, la de la m\xE1s nueva. Con `expected_updated_at` evitas pisar un cambio m\xE1s nuevo: si no es el \xFAltimo guardado responde `409` `STALE`.\n 3. **Valida en Meta (opcional):** `POST /forms/{id}/sync` corre el linter, sube el formulario a Meta y trae sus observaciones en `lint` con `code` `meta`, sin publicar. Si la definici\xF3n tiene errores responde `422` `LINT`, igual que al publicar.\n 4. **Publica:** `POST /forms/{id}/publish` hace la validaci\xF3n y publica; es inmediato. Si la definici\xF3n tiene errores responde `422` `LINT` con la lista, y si Meta la rechaza, `502` `META` (`422` `META` si la rechaza porque tu cuenta de WhatsApp Business no cumple sus requisitos de integridad). Un formulario sin ninguna pregunta no se puede publicar. Los otros n\xFAmeros del formulario se publican en segundo plano: consulta su estado (`draft`, `publishing`, `published` o `failed`) en `GET /forms/{id}/numbers`.\n 5. **Suma o quita n\xFAmeros (opcional):** `POST /forms/{id}/numbers` con `device_id` publica el formulario en otro n\xFAmero de WhatsApp Cloud de tu cuenta, `POST /forms/{id}/numbers/retry` reintenta los que fallaron y `DELETE /forms/{id}/numbers/{deviceId}` quita uno. El n\xFAmero con el que creaste el formulario no se quita: `422` `PRIMARY_NUMBER`. Un n\xFAmero que no es de tu cuenta o no es WhatsApp Cloud responde `422` `NUMBER_NOT_ELIGIBLE`.\n 6. **Env\xEDa:** `POST /contacts/{contact}/message/form` con `form_id` (el formulario, no la versi\xF3n) y `device_id`; opcionalmente `text` (hasta 1024 caracteres), `button_text` (hasta 30, sin emojis) y `client_reference`. S\xF3lo se env\xEDa con una clave de API. La clave de un asiento s\xF3lo env\xEDa a los contactos asignados a ese asiento (con cualquier otro contacto responde el `404` de un contacto que no existe); la clave del titular env\xEDa a cualquier contacto de la cuenta. Responde `200` con `message` en cola (`queued`); sus errores propios est\xE1n al final de esta gu\xEDa.\n 7. **Recibe las respuestas:** suscr\xEDbete al evento `whatsapp_form.completed` (tambi\xE9n llega si te suscribes a `*`) o lee `GET /forms/families/{familyId}/responses` y `GET /forms/families/{familyId}/responses/{runId}`. `GET /forms/families/{familyId}/metrics` trae los env\xEDos, aperturas y respuestas.\n - **Cambiar un formulario publicado:** un formulario publicado no se edita (`PUT` y `DELETE` responden `409` `PUBLISHED`). Crea una versi\xF3n nueva con `POST /forms/{id}/versions`, que nace como borrador (`201`, o `200` con `created: false` si ya hab\xEDa un borrador), ed\xEDtala y publ\xEDcala. El `form_id` no cambia: el env\xEDo usa la versi\xF3n publicada vigente.\n- **Reglas de la definici\xF3n:** el servidor no genera ni corrige identificadores: quien arma la definici\xF3n los genera, y deben ser v\xE1lidos y \xFAnicos. Meta acepta s\xF3lo letras y guiones bajos.\n - **Pantallas:** `id` con `SCREEN_` m\xE1s letras (por ejemplo `SCREEN_ABCD`), sin d\xEDgitos, y \xFAnicos en el formulario. `next` es `auto` (la siguiente), `complete` (termina) o el `id` de una pantalla posterior: s\xF3lo se salta hacia adelante.\n - **Preguntas:** `name` en min\xFAsculas y guiones bajos (`a-z` y `_`), \xFAnico en todo el formulario. Sin d\xEDgitos, tildes, `\xF1` ni espacios. No uses los nombres reservados `flow_token`, `version` ni `extension_message_response`. No cambies un `name` despu\xE9s de crearlo: las respuestas, las ramas y las etiquetas lo usan.\n - **Opciones:** `id` de cada opci\xF3n en min\xFAsculas y guiones bajos, \xFAnico en su pregunta (por ejemplo `op_comercio`).\n - **Qu\xE9 marca el linter:** `field_name_reserved` (nombre reservado), `field_name_invalid` (nombre fuera de letras y guiones bajos) y `option_id_invalid` (id de opci\xF3n fuera de ese alfabeto) son errores que impiden validar y publicar. El linter no revisa que los identificadores sean \xFAnicos ni que existan los campos personalizados y las etiquetas que referencias: compru\xE9balo t\xFA.\n - **Campos personalizados y etiquetas:** si una pregunta guarda su respuesta en un campo personalizado (`map` con `target` `custom` y `key` con el nombre del campo) o asigna etiquetas (`tagMap`, con el nombre de la etiqueta), ese campo y esa etiqueta tienen que existir en tu cuenta. El formulario no los crea: si no existen, la respuesta se guarda en el formulario pero no se escribe en el contacto ni se le asigna la etiqueta. Un campo num\xE9rico s\xF3lo admite preguntas `number` y uno de fecha s\xF3lo preguntas `date`; el servidor no lo revisa.\n - **No se pueden publicar** los formularios que necesitan un servidor propio (`endpoint` en `true`, opciones en vivo): el linter los marca como error.\n- **L\xEDmites del formulario:** hasta 5 pantallas, 50 elementos por pantalla, 3 im\xE1genes por pantalla y 2 enlaces por pantalla; pasarse es un error que impide publicar. El nombre del formulario admite hasta 120 caracteres. Las im\xE1genes (`type` `image`, con `src` en base64 sin el prefijo `data:`) deben ser JPG o PNG, de hasta 300 KB cada una y 4 MB entre todas las del formulario; el servidor no revisa su tama\xF1o ni su formato, as\xED que quien arma la definici\xF3n debe respetarlos. El guardado acepta un `src` con el prefijo `data:`, pero se env\xEDa a Meta tal como lo guardaste y Meta dibuja un recuadro roto: manda siempre la carga en base64 pelada, sin prefijo.\n - **Tope de cada guardado:** adem\xE1s, un guardado rechaza (`422`) una definici\xF3n con m\xE1s de 20 pantallas, 100 elementos en una pantalla o 200 opciones en una pregunta. Esos topes quedan por encima de los l\xEDmites de arriba. Adem\xE1s, cada guardado env\xEDa la definici\xF3n entera, im\xE1genes incluidas, y debe pesar menos de unos 6 MB: pasado ese tama\xF1o la plataforma rechaza la llamada antes de que llegue al formulario, as\xED que no cuentes con m\xE1s.\n- **Qu\xE9 puede hacer cada clave:** los permisos se heredan hacia abajo: **Contactos y conversaciones \u2192 Enviar** incluye **Contactos y conversaciones \u2192 Editar**, que incluye **Contactos y conversaciones \u2192 Ver**. Con **Contactos y conversaciones \u2192 Ver** lees formularios, su definici\xF3n (`GET /forms/families/{familyId}/definition`), versiones, n\xFAmeros, respuestas y m\xE9tricas; con **Contactos y conversaciones \u2192 Editar** adem\xE1s armas, guardas, publicas, validas, creas versiones, eliminas borradores y sumas, reintentas o quitas n\xFAmeros; con **Contactos y conversaciones \u2192 Enviar** adem\xE1s env\xEDas. Por eso una clave con **Contactos y conversaciones \u2192 Enviar**, aunque s\xF3lo la hayas pensado para enviar, tambi\xE9n puede armar, publicar y quitar n\xFAmeros de formularios: dale ese permiso s\xF3lo a quien conf\xEDas para eso. Las operaciones `summary`, `analysis` y `deliveries` del panel no est\xE1n abiertas a claves.\n - **Topes de llamadas por clave:** cada clave puede hacer hasta 120 lecturas por minuto y hasta 60 escrituras por minuto en formularios (cada clave lleva su propio contador: dos claves de la misma cuenta, sea del titular o de un asiento, no se frenan entre s\xED, y ninguna gasta el tope del panel). Al superarlos la respuesta es `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar. Validar, publicar y tocar n\xFAmeros tienen adem\xE1s el tope de 30 por minuto compartido por toda la cuenta, que se explica en los errores al final de esta gu\xEDa.\n - **Cuenta:** la clave de un asiento (un agente) opera sobre los formularios del titular de la cuenta, no sobre formularios propios del asiento.\n - **Respuestas con clave de un asiento:** `responses` y `responses/{runId}` se limitan a los contactos que ese asiento puede ver en Funnelchat; las m\xE9tricas (`metrics`) cuentan todas las respuestas del formulario.\n - **Detalle de una respuesta con clave:** `GET /forms/families/{familyId}/responses/{runId}` no trae `last_error` en sus efectos; el resto de cada efecto es igual al del panel.\n- **Aviso `whatsapp_form.completed`:** `data.form` trae `id` (el formulario), `name` y `version_id` (la versi\xF3n que recibi\xF3 el contacto); `message_id` es el mensaje del formulario y puede ser `null`; `answers` es una lista de `{key, label, value}`, una por pregunta y en el orden del formulario (`key` es el `name` de la pregunta, y `value` es `null` si no la contest\xF3). El mismo aviso puede llegar m\xE1s de una vez con el mismo `id`: deduplica por `id`. Cada respuesta de texto llega con a lo sumo 600 caracteres. Si el contacto vuelve a enviar el mismo formulario no se genera otro aviso: queda la primera respuesta. Los detalles y el ejemplo est\xE1n en la secci\xF3n \xABWebhooks\xBB.\n- **Errores al armar y publicar:** `404` si el `id` no existe o no es de tu cuenta (un id de otra cuenta responde igual); `409` `STALE` (el guardado no es el \xFAltimo) y `409` `PUBLISHED` (la versi\xF3n ya est\xE1 publicada); `422` `LINT` al publicar con errores en la definici\xF3n, `422` `NUMBER_NOT_ELIGIBLE` y `422` `PRIMARY_NUMBER` en los n\xFAmeros; `502` `META` si Meta rechaza la operaci\xF3n o no responde, y `422` `META` si la rechaza porque tu cuenta de WhatsApp Business no cumple sus requisitos de integridad. Validar, publicar y tocar n\xFAmeros (`sync`, `publish` y las operaciones de `numbers`) sube a Meta, que limita por cuenta de WhatsApp Business: todas las claves de tu cuenta, la del titular y las de los asientos, comparten un tope de 30 de estas llamadas por minuto. Al superarlo responde `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar.\n- **Errores al enviar un formulario:** adem\xE1s de los errores de las reglas de protecci\xF3n de esta documentaci\xF3n, `POST /contacts/{contact}/message/form` responde: `404` si el contacto no existe, no es de tu cuenta o, con la clave de un asiento, no est\xE1 asignado a ese asiento; `404` con `form_not_found` si el formulario no existe en tu cuenta; `422` con `device_unavailable` (el n\xFAmero no es de tu cuenta o no est\xE1 conectado), `number_not_eligible` (no es un n\xFAmero de WhatsApp Cloud que pueda enviar formularios), `form_not_published` (el formulario no tiene versi\xF3n publicada: publ\xEDcalo), `form_not_on_number` (no est\xE1 publicado en ese n\xFAmero: s\xFAmalo con `POST /forms/{id}/numbers` y espera a que se publique), `recipient_unreachable` (el contacto no tiene tel\xE9fono ni identificador de WhatsApp al que ese n\xFAmero pueda escribir), `contact_blocked` (bloqueaste al contacto en ese n\xFAmero: desbloqu\xE9alo para escribirle) y `window_closed` (pasaron m\xE1s de 24 horas desde el \xFAltimo mensaje del contacto); `429` con `rate_limited` y `Retry-After` si superas el ritmo; y `422` con `not_sent` (el destinatario que se registr\xF3 no coincide con el contacto verificado, o el mensaje no qued\xF3 como un env\xEDo de la conversaci\xF3n: no se registr\xF3 ni sali\xF3 nada y puedes reintentar; son las \xFAnicas dos situaciones y en el uso normal no ocurren). Si Funnelchat no pudo poner el mensaje en cola, la respuesta no es un error: es `200` con el mensaje en estado `failed` y `error.reason` `not_queued`; no sali\xF3, as\xED que reintentar no lo duplica. Si tu cuenta est\xE1 bloqueada por el l\xEDmite de contactos activos, el env\xEDo responde `429` con `mac_lockout_24h`, como los dem\xE1s env\xEDos con clave, pero a diferencia de ellos no guarda ning\xFAn mensaje ni `client_reference`: no queda nada en el historial del contacto ni se puede buscar por la referencia, as\xED que reintenta cuando termine el bloqueo. Un `500` indica que el mensaje no pudo registrarse. Si la llamada no responde o da un error 5xx, busca tu `client_reference` en el historial del contacto antes de reintentar.'
|
|
51492
|
+
description: 'Public API of Funnelchat Conversations. Authenticate every request with `Authorization: Bearer <id>|fc_live_...`. Each operation shows the permission your key needs, as the Developers panel names it: **Contacts and conversations \u2192 View**, **Contacts and conversations \u2192 Edit** or **Contacts and conversations \u2192 Send**.\n\n## Webhooks\n\nLos webhooks avisan a un sistema externo, por HTTP, cuando algo pasa en tu cuenta, sin que tengas que consultar la API cada tanto. Registras una URL HTTPS, eliges los tipos de evento que quieres recibir y Funnelchat env\xEDa a esa URL un `POST` firmado cada vez que ocurre uno. Las rutas de administraci\xF3n requieren una clave de API y que la use el titular de la cuenta.\n\n### C\xF3mo suscribirse\n\nEstas son las nueve rutas para administrar suscripciones:\n\n| Ruta | Para qu\xE9 sirve | Permiso |\n| --- | --- | --- |\n| `GET /api/v1/webhooks` | Listar las suscripciones | Contactos y conversaciones \u2192 Ver |\n| `POST /api/v1/webhooks` | Crear una suscripci\xF3n | Contactos y conversaciones \u2192 Editar |\n| `GET /api/v1/webhooks/events` | Ver el cat\xE1logo de tipos de evento | Contactos y conversaciones \u2192 Ver |\n| `GET /api/v1/webhooks/{webhook}` | Ver una suscripci\xF3n | Contactos y conversaciones \u2192 Ver |\n| `PUT /api/v1/webhooks/{webhook}` | Cambiar URL, tipos o estado activo | Contactos y conversaciones \u2192 Editar |\n| `DELETE /api/v1/webhooks/{webhook}` | Eliminar una suscripci\xF3n | Contactos y conversaciones \u2192 Editar |\n| `POST /api/v1/webhooks/{webhook}/rotate-secret` | Generar un secreto nuevo | Contactos y conversaciones \u2192 Editar |\n| `POST /api/v1/webhooks/{webhook}/test` | Enviar ahora un evento de prueba | Contactos y conversaciones \u2192 Editar |\n| `GET /api/v1/webhooks/{webhook}/deliveries` | Ver el historial de entregas | Contactos y conversaciones \u2192 Ver |\n\nEjemplo de alta:\n\n```bash\ncurl -X POST "https://TU-HOST/api/v1/webhooks" \\\n -H "Authorization: Bearer <id>|fc_live_..." \\\n -H "Content-Type: application/json" \\\n -d \'{\n "url": "https://example.com/hooks/funnelchat",\n "description": "Mi integraci\xF3n",\n "events": ["message.received"]\n }\'\n```\n\nRespuesta `201`:\n\n```json\n{\n "data": {\n "uuid": "00000000-0000-4000-8000-000000000001",\n "url": "https://example.com/hooks/funnelchat",\n "description": "Mi integraci\xF3n",\n "events": ["message.received"],\n "active": true,\n "inactive_reason": null,\n "created_at": "2026-10-01T12:00:00+00:00",\n "updated_at": "2026-10-01T12:00:00+00:00",\n "secret": "<secreto de firma>"\n }\n}\n```\n\nEl `secret` se muestra **una sola vez**, en esta respuesta (y al rotarlo). Gu\xE1rdalo: no se puede volver a consultar. En `events` puedes usar los tipos `message.received`, `flow.completed`, `broadcast.finished`, `contact.stage_changed`, `message.status_changed`, `conversation.closed`, `conversation.assigned`, `contact.tag_added`, `broadcast.paused`, `contact.tag_removed`, `contact.created`, `template.status_changed`, `flow.failed`, `whatsapp_form.completed` o `*` para todos los dem\xE1s que se puedan suscribir (`message.status_changed` siempre se suscribe de forma expl\xEDcita: `*` no lo incluye). La URL debe ser `https` y apuntar a un destino p\xFAblico.\n\n### El sobre\n\nCada entrega es un `POST` con un cuerpo JSON con esta forma. El contenido de `data` depende del tipo de evento (ver el cat\xE1logo m\xE1s abajo).\n\n| Campo | Descripci\xF3n |\n| --- | --- |\n| `id` | Identificador \xFAnico del evento (`evt_...`). Es el mismo en todos los reintentos. |\n| `type` | Tipo de evento, por ejemplo `message.received`. |\n| `version` | Versi\xF3n del formato del evento (hoy `1`). |\n| `occurred_at` | Momento en que ocurri\xF3, en UTC (ISO 8601). |\n| `data` | Datos del evento. |\n\n### Cabeceras\n\n| Cabecera | Contenido |\n| --- | --- |\n| `Content-Type` | `application/json` |\n| `X-Funnelchat-Signature` | Firma de la entrega, con la forma `t=<timestamp>,v1=<firma>` |\n| `X-Funnelchat-Event` | Tipo de evento (igual que `type` en el cuerpo) |\n| `X-Funnelchat-Delivery` | Identificador de esta entrega (cambia en cada intento) |\n\n### C\xF3mo verificar la firma\n\nCada entrega lleva la cabecera `X-Funnelchat-Signature` con la forma `t=<timestamp>,v1=<firma>`. Para verificarla:\n\n1. Toma el **cuerpo crudo** de la petici\xF3n, tal como lleg\xF3, sin volver a codificar el JSON.\n2. Arma el texto `t + "." + cuerpo` (el timestamp, un punto y el cuerpo).\n3. Calcula su HMAC-SHA256 con el secreto de la suscripci\xF3n y comp\xE1ralo, en tiempo constante, con `v1`.\n4. Rechaza la entrega si `t` se aleja del reloj de tu servidor m\xE1s de 300 segundos.\n\nEjemplo en PHP:\n\n```php\nfunction verificarFirma(string $cuerpoCrudo, string $cabecera, string $secreto, int $tolerancia = 300): bool\n{\n parse_str(str_replace(\',\', \'&\', $cabecera), $partes);\n\n if (! isset($partes[\'t\'], $partes[\'v1\']) || ! ctype_digit($partes[\'t\'])) {\n return false;\n }\n\n if (abs(time() - (int) $partes[\'t\']) > $tolerancia) {\n return false;\n }\n\n $esperada = hash_hmac(\'sha256\', $partes[\'t\'].\'.\'.$cuerpoCrudo, $secreto);\n\n return hash_equals($esperada, $partes[\'v1\']);\n}\n```\n\nEjemplo en JavaScript (Node.js):\n\n```javascript\nconst crypto = require(\'crypto\');\n\nfunction verificarFirma(cuerpoCrudo, cabecera, secreto, tolerancia = 300) {\n const partes = Object.fromEntries(cabecera.split(\',\').map((p) => p.split(\'=\')));\n if (!/^\\d+$/.test(partes.t || \'\') || !partes.v1) return false;\n\n if (Math.abs(Date.now() / 1000 - Number(partes.t)) > tolerancia) return false;\n\n const esperada = crypto\n .createHmac(\'sha256\', secreto)\n .update(`${partes.t}.${cuerpoCrudo}`)\n .digest(\'hex\');\n\n const a = Buffer.from(esperada);\n const b = Buffer.from(partes.v1);\n\n return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\nEn JavaScript, lee el cuerpo como texto o `Buffer` antes de convertirlo a objeto (por ejemplo con `express.raw({ type: \'application/json\' })`).\n\n### Reintentos\n\nUna entrega es exitosa si tu servidor responde con un c\xF3digo `2xx` en 10 segundos o menos. Funnelchat no sigue redirecciones.\n\n- Se hacen hasta **4 intentos** por evento y suscripci\xF3n. Entre un intento y el siguiente se espera 60, 300, 1800 segundos.\n- Se reintenta cuando hay un error de red o un timeout, ante un `5xx` y ante un `429`.\n- Un `3xx` o cualquier otro `4xx` es terminal: no se reintenta.\n- Despu\xE9s del \xFAltimo intento fallido la entrega queda como `failed`, sin m\xE1s reintentos.\n\n### Estados de entrega\n\n`GET /api/v1/webhooks/{webhook}/deliveries` devuelve un registro por intento, con estos estados:\n\n| Estado | Significado |\n| --- | --- |\n| `pending` | El intento est\xE1 en curso: todav\xEDa no hay resultado. |\n| `succeeded` | Tu servidor respondi\xF3 `2xx`. |\n| `failed` con `next_attempt_at` | El intento fall\xF3 y hay otro programado para esa fecha. |\n| `failed` sin `next_attempt_at` | El intento fall\xF3 y no habr\xE1 m\xE1s: era terminal o se agotaron los intentos. |\n\n### Probar una suscripci\xF3n\n\n`POST /api/v1/webhooks/{webhook}/test` env\xEDa ahora un evento `webhook.test` firmado a la URL de la suscripci\xF3n, sin esperar a que ocurra nada en tu cuenta, y responde con el resultado del intento. Funciona tambi\xE9n sobre una suscripci\xF3n inactiva, para probarla antes de activarla.\n\nLa respuesta es un \xEDtem del historial de entregas, con la misma forma que cada elemento de `GET /api/v1/webhooks/{webhook}/deliveries`:\n\n```json\n{\n "data": {\n "uuid": "0d3c2f4e-6a1b-4c8d-9e2f-5b7a1c3d4e6f",\n "event_id": "evt_01hexample",\n "event_type": "webhook.test",\n "attempt": 1,\n "status": "succeeded",\n "http_status": 200,\n "duration_ms": 184,\n "error": null,\n "next_attempt_at": null,\n "created_at": "2026-01-15T12:00:00+00:00",\n "updated_at": "2026-01-15T12:00:00+00:00"\n}\n}\n```\n\n`status` es `succeeded` solo si tu servidor respondi\xF3 `2xx`; si no, es `failed` y `error` indica la causa. La prueba aparece en el historial con `event_type: "webhook.test"`, no se reintenta y no consume tu cupo de eventos.\n\n### Suscripciones creadas con una clave de API\n\nUna suscripci\xF3n creada, o reactivada, con una clave de API queda atada a esa clave. Tambi\xE9n la atan a una clave cambiar su URL con esa clave, o rotar su secreto con esa clave (rotarlo desde la sesi\xF3n web no modifica el v\xEDnculo): la suscripci\xF3n queda atada a la \xFAltima credencial que fij\xF3 su destino. Si la clave se revoca (o vence, que cuenta como revocada), la suscripci\xF3n deja de recibir eventos: pasa a `active: false` con `inactive_reason: "key_revoked"`, y no se le env\xEDan m\xE1s eventos.\n\nEse cambio se ve a partir del primer evento que ocurra despu\xE9s de revocar la clave. Hasta entonces, un `GET` puede seguir mostr\xE1ndola activa, pero no recibe eventos.\n\nLa prueba manual (`POST /api/v1/webhooks/{webhook}/test`) sigue funcionando sobre una suscripci\xF3n inactiva.\n\nUna suscripci\xF3n con `inactive_reason: "key_revoked"` no se puede reactivar: `PUT /api/v1/webhooks/{webhook}` con `"active": true` responde 422, sin importar la credencial que se use. Para seguir recibiendo eventos, crea una suscripci\xF3n nueva con `POST /api/v1/webhooks`. Una suscripci\xF3n que desactivaste a mano s\xED se puede reactivar con `PUT` y `"active": true`; queda atada a la credencial que la reactiva. Mientras est\xE9 activa, `inactive_reason` es `null`.\n\nLas suscripciones cuyo destino fij\xF3 por \xFAltima vez la sesi\xF3n web no dependen de ninguna clave: revocar una clave no las afecta.\n\n### Idempotencia\n\nUn mismo evento puede llegarte m\xE1s de una vez (por ejemplo, si tu servidor proces\xF3 la entrega pero respondi\xF3 tarde). Usa el campo `id` del sobre para descartar los que ya procesaste: es el mismo en todos los reintentos del evento.\n\n### Eventos disponibles\n\nSe generan desde el cat\xE1logo vigente. Los tipos marcados como **planificado** todav\xEDa no se emiten: se anuncian para que puedas prepararte, pero no los recibir\xE1s.\n\n`webhook.test` solo se env\xEDa con `POST /api/v1/webhooks/{webhook}/test`.\n\n#### `message.received`\n\nUn contacto le escribi\xF3 a un n\xFAmero de WhatsApp de la cuenta.\n\n```json\n{\n "id": "evt_000000000000000000000001",\n "type": "message.received",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "message": {\n "id": 1001,\n "type": "text",\n "text": "Hola, quiero m\xE1s informaci\xF3n",\n "media": null\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "receiver": {\n "phone": "+5491100000001"\n },\n "device_id": 4521\n }\n}\n```\n\nValores posibles de `data.message.type`: `text`, `image`, `video`, `document`, `sticker`, `contact`, `audio`, `note`, `action`, `invalid`, `interactive`, `location`, `voice_call`, `video_call`, `event`, `button`, `template`, `notification`, `option_list`, `template_button_reply`, `poll`, `whatsapp_form`, `call_permission_request`, `call_permission_reply`, `request_contact_info`.\n\n`device_id` es el n\xFAmero de tu cuenta que recibi\xF3 el mensaje: el mismo `device_id` que mandas para responder por ese n\xFAmero. Es `null` si el mensaje no tiene n\xFAmero asociado.\n\n#### `flow.completed`\n\nUn contacto termin\xF3 un flujo de automatizaci\xF3n.\n\n```json\n{\n "id": "evt_000000000000000000000003",\n "type": "flow.completed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "flow": {\n "id": 3001,\n "name": "Flujo de ejemplo"\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n }\n }\n}\n```\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.\n\n#### `broadcast.finished`\n\nUna campa\xF1a lleg\xF3 a su estado final (enviada, fallida o con fallos parciales), con sus conteos.\n\n```json\n{\n "id": "evt_000000000000000000000011",\n "type": "broadcast.finished",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "broadcast": {\n "id": 6001,\n "name": "Campa\xF1a de ejemplo"\n },\n "status": "partially_failed",\n "reason": "completed",\n "totals": {\n "processed": 1200,\n "failed": 30,\n "skipped_unreachable": 12\n }\n }\n}\n```\n\n`status` es `success`, `failed` o `partially_failed`. `reason` dice por qu\xE9 termin\xF3: `completed` (se enviaron todas las p\xE1ginas), `cancelled` (se cerr\xF3 una campa\xF1a pausada), `template_rejected` (Meta rechaz\xF3 la plantilla, o su categor\xEDa cambi\xF3, mientras la campa\xF1a esperaba; tambi\xE9n cuando Meta la paus\xF3 durante el env\xEDo y se detuvo la campa\xF1a), `approval_expired` (la plantilla no se aprob\xF3 a tiempo), `mac_lockout` (la cuenta alcanz\xF3 su l\xEDmite de contactos activos), `mac_check_unavailable` (la campa\xF1a la inici\xF3 una API key y el l\xEDmite de contactos activos no se pudo verificar, as\xED que no se envi\xF3 nada), `no_recipients` (la audiencia qued\xF3 vac\xEDa) o `error` (el env\xEDo no pudo despacharse). `totals` son los conteos del env\xEDo: `processed` (destinatarios procesados), `failed` y `skipped_unreachable` (destinatarios que el canal no puede alcanzar); valen 0 si la campa\xF1a termin\xF3 antes de enviar. Se emite una sola vez por campa\xF1a.\n\n#### `contact.stage_changed`\n\nUn contacto cambi\xF3 de etapa en un tablero.\n\n```json\n{\n "id": "evt_000000000000000000000008",\n "type": "contact.stage_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "board": {\n "id": 5001,\n "name": "Embudo de ejemplo"\n },\n "kind": "move",\n "from_stage": {\n "id": 6001,\n "name": "Nuevo",\n "position": 0\n },\n "to_stage": {\n "id": 6002,\n "name": "En seguimiento",\n "position": 1\n }\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto que cambia de etapa en un tablero por etapas. `kind` es entry (entra al tablero, `from_stage` es null), move (pasa de una etapa a otra), exit (sale del tablero, `to_stage` es null) o stage_deleted (se reubica porque su etapa se borr\xF3; el nombre de la etapa borrada es null). No se env\xEDa si el contacto se coloca en la etapa en la que ya estaba.\n\n#### `message.status_changed`\n\nCambi\xF3 el estado de entrega de un mensaje enviado.\n\n```json\n{\n "id": "evt_000000000000000000000014",\n "type": "message.status_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "message_id": 1001,\n "contact_id": 2001,\n "device_id": 4521,\n "status": "failed",\n "previous_status": "sent",\n "error": {\n "code": 131047,\n "title": "Re-engagement message",\n "reason": null\n },\n "occurred_at": "2026-10-01T12:00:00Z",\n "client_reference": "pedido-1001"\n }\n}\n```\n\nSe emite cuando cambia el estado de entrega de un mensaje que enviaste a un contacto, ya sea desde el chat, una automatizaci\xF3n o un asistente de IA (incluye los enviados con clave). No se emite para mensajes de difusiones ni para mensajes recibidos. `message_id` es el id que devuelve la API, `contact_id` el del contacto y `device_id` el n\xFAmero de tu cuenta por el que sali\xF3 el mensaje, el mismo `device_id` que usas para enviar (`null` si no tiene). `status` es `sent`, `delivered`, `read` o `failed`. `previous_status` es el estado que el mensaje ten\xEDa justo antes de este cambio y es `null` en tres casos: es el primer aviso del mensaje, es el mismo estado que `status` (por ejemplo, el proveedor confirma el `sent` que ya registr\xF3 el env\xEDo) o no se pudo leer. `error` solo viene cuando `status` es `failed`: `code` es el c\xF3digo num\xE9rico del proveedor y `title` su t\xEDtulo, y cualquiera de los dos puede ser `null` si el proveedor no los informa. `error.reason` es `null` cuando el fallo lo inform\xF3 el proveedor; cuando Funnelchat decidi\xF3 que un mensaje enviado con clave no sal\xEDa (por ejemplo, por saldo insuficiente o ventana de 24 horas cerrada) trae el motivo (`device_unavailable`, `insufficient_balance`, `window_closed`, `recipient_unreachable`, `template_not_found`, `template_unresolved_placeholder`, `not_queued` o `not_sent`), `code` es `null` y `title` es un texto llano en el idioma de la cuenta. `occurred_at` es el momento en que Funnelchat registr\xF3 el estado, en UTC. `client_reference` es la referencia que mandaste al enviar el mensaje (por ejemplo `pedido-1001`) y `null` si el env\xEDo no la llevaba: sirve para relacionar cada aviso con tu env\xEDo tambi\xE9n cuando la llamada de env\xEDo no respondi\xF3. Cada estado se informa una sola vez por mensaje: si llegan varios avisos juntos se informa el m\xE1s avanzado y un aviso atrasado que no cambia el estado no genera evento. Un mensaje que no lleg\xF3 a salir (por ejemplo, por ventana de 24 horas cerrada) no recibe avisos de estado, salvo uno enviado con clave: ese recibe un \xFAnico aviso `failed` con `previous_status` en `null`, su `error.reason` y su `client_reference`. Un mensaje enviado con clave cuyo resultado no se conoce (el proveedor no respondi\xF3 o el env\xEDo se interrumpi\xF3) no recibe aviso. Un mensaje cuyo env\xEDo no empez\xF3 dentro de la hora ya no sale: pasada la hora sin resultado, su estado es `null`, o `failed` con `error.reason` `not_sent` y su aviso si su env\xEDo llega tarde. Reintentar es un env\xEDo nuevo, que s\xF3lo puede duplicar si el env\xEDo hab\xEDa empezado y se cort\xF3. Un mensaje `queued` no es enviado: su resultado llega por este evento o por la consulta del mensaje. En cuentas con mucho tr\xE1fico, los eventos que excedan el l\xEDmite de entrega por minuto de la cuenta se descartan. Para recibirlo debes suscribirte a `message.status_changed` de forma expl\xEDcita: una suscripci\xF3n a `*` no lo incluye.\n\n#### `conversation.closed`\n\nUna conversaci\xF3n pas\xF3 a cerrada.\n\n```json\n{\n "id": "evt_000000000000000000000004",\n "type": "conversation.closed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n }\n }\n}\n```\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.\n\n#### `conversation.assigned`\n\nUna conversaci\xF3n qued\xF3 asignada a una persona del equipo o a un agente de IA.\n\n```json\n{\n "id": "evt_000000000000000000000005",\n "type": "conversation.assigned",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "assignee": {\n "type": "human",\n "name": "Agente de ejemplo"\n }\n }\n}\n```\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.\n\n#### `contact.tag_added`\n\nUn contacto recibi\xF3 una etiqueta.\n\n```json\n{\n "id": "evt_000000000000000000000006",\n "type": "contact.tag_added",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "tag": {\n "id": 4001,\n "name": "Etiqueta de ejemplo"\n },\n "source": "manual"\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto y etiqueta agregados, tambi\xE9n en operaciones en bloque; no se env\xEDa si el contacto ya ten\xEDa la etiqueta. `source` indica el origen: manual, flow, bulk, import, sync, walink o ai. En operaciones en bloque grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `broadcast.paused`\n\nUna campa\xF1a se paus\xF3 sola, por ejemplo porque baj\xF3 la calidad del n\xFAmero.\n\n```json\n{\n "id": "evt_000000000000000000000007",\n "type": "broadcast.paused",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "broadcast": {\n "id": 6001,\n "name": "Campa\xF1a de ejemplo"\n },\n "reason": "quality_degraded",\n "quality": "YELLOW",\n "quality_baseline": "GREEN",\n "template": null\n }\n}\n```\n\n`reason` es `quality_degraded` (la calidad del n\xFAmero baj\xF3 respecto de la del inicio, o lleg\xF3 a RED), `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos), `template_unavailable` (Meta dej\xF3 la plantilla en `PAUSED`, `DISABLED` o `REJECTED` antes de una tanda), `payment_issue` (el 2 % o m\xE1s de los env\xEDos de la misma campa\xF1a, con un m\xEDnimo de 20 env\xEDos, fueron rechazados por Meta con el c\xF3digo 131042, un problema de pago o de facturaci\xF3n de la cuenta de WhatsApp Business; el 2 % y el m\xEDnimo de 20 env\xEDos son los valores por defecto y se pueden configurar; aplica a cualquier campa\xF1a, tenga o no env\xEDo inteligente; no frena el n\xFAmero; al reanudar la campa\xF1a el conteo, incluidos los env\xEDos que cuentan, vuelve a empezar de cero) o `rejection_rate` (entre los env\xEDos recientes de la propia campa\xF1a, por defecto las \xFAltimas 24 horas, Meta rechaz\xF3 una proporci\xF3n igual o mayor al umbral configurable, por defecto 15 %, con los c\xF3digos que indican una restricci\xF3n de la cuenta o del n\xFAmero; la lista es configurable y por defecto es 131048, 131031 y 368; los c\xF3digos 131049 y 131026 no pausan la campa\xF1a). Con `rejection_rate` llega adem\xE1s `rejection`: `codes` (los que llegaron a su umbral: hoy `failed`), `rates` (el porcentaje de cada uno; las tasas de `131049` y `131026` se informan como dato aunque no pausen), `attempts` (los env\xEDos medidos) y `window_hours`. `quality` y `quality_baseline` son la calidad de Meta le\xEDda en el control y la del inicio del env\xEDo (`GREEN`, `YELLOW`, `RED`), o `null` si no se pudo leer, y siempre `null` en una difusi\xF3n sin env\xEDo inteligente. `template` es `{name, status}` con el nombre de la plantilla y su estado en Meta cuando `reason` es `template_unavailable`, y `null` en las dem\xE1s pausas (por calidad, pago o rechazos). Con `rejection_rate` el n\xFAmero adem\xE1s queda frenado 7 d\xEDas (configurable): mientras dure no se pueden crear, ejecutar ni reanudar campa\xF1as desde ese n\xFAmero, ni mandar plantillas a contactos que no le escribieron en las \xFAltimas 24 horas, y solo soporte lo levanta antes. Las pausas por calidad, por plantilla y por rechazos son solo de las campa\xF1as con env\xEDo inteligente; la pausa por `payment_issue` puede ocurrir en cualquier campa\xF1a. Reanudar la campa\xF1a y que se pause otra vez emite otro evento.\n\n#### `contact.tag_removed`\n\nUn contacto perdi\xF3 una etiqueta.\n\n```json\n{\n "id": "evt_000000000000000000000009",\n "type": "contact.tag_removed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "tag": {\n "id": 4001,\n "name": "Etiqueta de ejemplo"\n },\n "source": "manual"\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto y etiqueta quitados, tambi\xE9n en operaciones en bloque; no se env\xEDa si el contacto no ten\xEDa la etiqueta. No se env\xEDa al borrar la etiqueta, al borrar o fusionar contactos ni al desvincular etiquetas de un tablero. `source` indica el origen: manual, flow, bulk o ai. En operaciones en bloque grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `contact.created`\n\nSe cre\xF3 un contacto nuevo en la cuenta.\n\n```json\n{\n "id": "evt_000000000000000000000010",\n "type": "contact.created",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000",\n "email": "contacto@example.com"\n }\n }\n}\n```\n\nSe env\xEDa un evento por cada contacto nuevo, sea cual sea el canal: un mensaje entrante, la creaci\xF3n manual, una importaci\xF3n o la API. No se env\xEDa cuando el contacto ya exist\xEDa y s\xF3lo se actualiza, ni para los contactos que la plataforma crea para sus propias notificaciones. En importaciones grandes el l\xEDmite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `template.status_changed`\n\nUna plantilla cambi\xF3 de estado de aprobaci\xF3n en Meta.\n\n```json\n{\n "id": "evt_000000000000000000000012",\n "type": "template.status_changed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "template": {\n "id": 7001,\n "uid": "1234567890",\n "name": "plantilla_ejemplo",\n "category": "MARKETING"\n },\n "previous_status": "PENDING",\n "status": "APPROVED",\n "reason": null\n }\n}\n```\n\n`template` identifica la plantilla igual que `GET /templates` (`id`, `uid`, `name`, `category`). `previous_status` y `status` son los estados tal como los devuelve la API (por ejemplo `PENDING`, `APPROVED`, `REJECTED`, `PAUSED`); `previous_status` es `null` si la plantilla no ten\xEDa estado. `reason` trae la observaci\xF3n de rechazo (traducida como en la API y limitada a 500 caracteres) cuando el cambio es un rechazo informado al crear la plantilla, y es `null` en el resto. No se emite si Meta reenv\xEDa el estado que la plantilla ya ten\xEDa. Si una misma plantilla de Meta figura en varias cuentas, cada cuenta recibe su propio evento.\n\n#### `flow.failed`\n\nLa ejecuci\xF3n de un flujo para un contacto termin\xF3 por un error.\n\n```json\n{\n "id": "evt_000000000000000000000013",\n "type": "flow.failed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "flow": {\n "id": 3001,\n "name": "Flujo de ejemplo"\n },\n "contact": {\n "id": 2001,\n "name": "Contacto de ejemplo",\n "phone": "+5491100000000"\n },\n "reason": "execution_error"\n }\n}\n```\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.\n\n#### `whatsapp_form.completed`\n\nUn contacto complet\xF3 un formulario de WhatsApp.\n\n```json\n{\n "id": "evt_000000000000000000000015",\n "type": "whatsapp_form.completed",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "form": {\n "id": 4001,\n "name": "Captar un lead",\n "version_id": 4002\n },\n "message_id": 1001,\n "contact_id": 2001,\n "device_id": 5001,\n "answers": [\n {\n "key": "nombre",\n "label": "Nombre",\n "value": "Contacto de ejemplo"\n },\n {\n "key": "intereses",\n "label": "Intereses",\n "value": [\n "op_a",\n "op_b"\n ]\n },\n {\n "key": "comentarios",\n "label": "Comentarios",\n "value": null\n }\n ],\n "occurred_at": "2026-10-01T12:00:00Z"\n }\n}\n```\n\nSe env\xEDa cuando un contacto completa un formulario, aunque la respuesta llegue tarde. Puede llegar m\xE1s de una vez con el mismo `id`: deduplica por `id`. Si el contacto vuelve a enviar el mismo formulario, no se genera otro aviso: queda la primera respuesta. Cada respuesta de texto llega con a lo sumo 600 caracteres. `form.id` es el formulario (el mismo `family_id` de la lectura de formularios) y `form.version_id` la versi\xF3n que recibi\xF3 el contacto. `answers` es una lista con una entrada por pregunta, en el orden del formulario: `key` es el nombre de la pregunta, `label` su etiqueta y `value` la respuesta (texto, n\xFAmero, `true` o `false`, una lista de textos en opci\xF3n m\xFAltiple, o `null` si no la contest\xF3). En las preguntas de lista y de opci\xF3n m\xFAltiple, `value` es el id de la opci\xF3n. `message_id` es el id del mensaje del formulario y es `null` si el env\xEDo no tuvo un mensaje p\xFAblico; `device_id` es el n\xFAmero de WhatsApp por el que sali\xF3, o `null` si ya no es de la cuenta. `contact_id` es `null` si el contacto se elimin\xF3 antes de que saliera el aviso (las respuestas llegan igual); si se fusion\xF3 con otro, es el id del contacto que qued\xF3. Mientras se fusionan dos contactos, el id puede ser el del contacto que se est\xE1 fusionando.\n\n#### `webhook.test`\n\nEvento de prueba enviado desde la ruta de prueba de una suscripci\xF3n.\n\n```json\n{\n "id": "evt_000000000000000000000002",\n "type": "webhook.test",\n "version": 1,\n "occurred_at": "2026-10-01T12:00:00Z",\n "data": {\n "subscription": "00000000-0000-4000-8000-000000000001",\n "sent_at": "2026-10-01T12:00:00Z"\n }\n}\n```\n\n### Otros webhooks de la plataforma\n\nLos webhooks de alertas y el nodo \xABPetici\xF3n HTTP\xBB de las automatizaciones son otros productos, con otro formato y otra configuraci\xF3n. No se administran con las rutas de esta secci\xF3n ni llevan este sobre ni esta firma.\n\n## Listados\n\nCon una clave de API, los listados siempre responden una p\xE1gina con la forma `{ "data": [...], "links": {...}, "meta": {...} }`: `data` trae los \xEDtems de la p\xE1gina, `links` las URL de `first`, `last`, `prev` y `next` (`null` cuando no hay p\xE1gina), y `meta` el detalle (`current_page`, `from`, `last_page`, `links`, `path`, `per_page`, `to`, `total`).\n\n- **Tama\xF1o de p\xE1gina:** el par\xE1metro `perPage` es un entero entre 1 y 100. Si no lo mandas, la p\xE1gina trae 25 \xEDtems. Un valor mayor que 100, menor que 1 o que no sea un entero responde 422 con el detalle en `errors.perPage`. Algunos listados aceptan adem\xE1s `per_page` como alias con los mismos l\xEDmites; si mandas `perPage` y `per_page` con valores distintos, la respuesta es 422 con el detalle en `errors.perPage`.\n- **Orden:** cada listado documenta los par\xE1metros de orden que admite y los valores permitidos de cada uno (por ejemplo `orderBy` y `orderByMethod`). Un par\xE1metro de orden que el listado no admite, o un valor fuera de la lista, responde 422 con el detalle en `errors.<par\xE1metro>`.\n- **P\xE1ginas siguientes:** las URL de `links` y de `meta.links` conservan `perPage`, el orden y los filtros que admite el listado; los dem\xE1s par\xE1metros no se copian y s\xF3lo cambia `page`. En los listados que se piden por `POST`, los filtros viajan en el cuerpo y las URL llevan s\xF3lo `perPage` y `page`.\n- **Primero el permiso:** si tu clave no tiene el permiso del listado, la respuesta es la de siempre (401 o 403), aunque mandes un `perPage` inv\xE1lido.\n\n## Env\xEDo de mensajes con clave\n\nCuando env\xEDas con una clave de API, antes de enviar se aplican reglas de protecci\xF3n de tu n\xFAmero y de tus contactos. Si una regla frena el env\xEDo, la respuesta es un JSON `{ "error": "<c\xF3digo>", "message": "<explicaci\xF3n>" }` y no se env\xEDa nada ni se registra ning\xFAn mensaje. Cada operaci\xF3n de env\xEDo documenta los c\xF3digos que puede responder.\n- **A qui\xE9n env\xEDas con `POST /message/send-message` y `POST /devices/{device}/templates/{template_id}/send-template`:** Indica a qui\xE9n env\xEDas con `phone`: el tel\xE9fono con c\xF3digo de pa\xEDs, por ejemplo `5491122334455`. No hace falta crear el contacto antes: si ese tel\xE9fono todav\xEDa no es un contacto de tu cuenta, se crea en el workspace de tu clave, igual que con `POST /contacts/update-or-create` y con el mismo permiso, que **Enviar** ya incluye. La respuesta trae su id en `message.contact_id`, el mismo que llevan los avisos `message.status_changed`. Si el tel\xE9fono no es v\xE1lido, o ya es un contacto de otro workspace de tu cuenta, la respuesta es la misma que dar\xEDa `POST /contacts/update-or-create`: 422, o 404 sin datos de ese contacto. Con un n\xFAmero oficial de WhatsApp (API Cloud) tambi\xE9n puedes indicar `whatsapp_id`, el BSUID que env\xEDa Meta (por ejemplo `US.13491208655302741918`), en lugar de `phone`; usado as\xED con un n\xFAmero conectado por QR, responde 422. En un n\xFAmero oficial, el primer mensaje a un contacto nuevo tiene que ser una plantilla: un mensaje libre solo sale dentro de las 24 horas desde que el contacto te escribi\xF3. Si ya tienes el `contact_id`, puedes seguir envi\xE1ndolo: si junto con \xE9l mandas `phone` o `whatsapp_id`, tienen que ser de ese mismo contacto (si no, 422 `recipient_mismatch`). En `send-template`, `phone` o `whatsapp_id` va siempre, con o sin `contact_id`.\n- **Ritmo:** el l\xEDmite depende s\xF3lo del tipo de n\xFAmero por el que env\xEDas; en los env\xEDos no hay un l\xEDmite por minuto por clave ni por cuenta. Al superarlo, la respuesta es 429 con `error` = `rate_limited` y el encabezado `Retry-After`, que trae los segundos que debes esperar antes de reintentar. Las reacciones s\xF3lo tienen esta regla.\n - **N\xFAmero de WhatsApp Cloud (API oficial):** hasta 60 mensajes por segundo y hasta 10 mensajes por minuto al mismo contacto.\n - **N\xFAmero en coexistencia** (el mismo tel\xE9fono conectado a WhatsApp Cloud y a la aplicaci\xF3n WhatsApp Business del celular): hasta 15 mensajes por segundo en total entre las dos conexiones, env\xEDes por la que env\xEDes. El l\xEDmite de 10 mensajes por minuto al mismo contacto aplica s\xF3lo a lo que env\xEDas por WhatsApp Cloud.\n - **N\xFAmero conectado por c\xF3digo QR** (sin coexistencia): esta regla no limita sus env\xEDos.\n- **Ventana de 24 horas:** por un n\xFAmero de WhatsApp Cloud s\xF3lo puedes enviar mensajes que no son plantilla a un contacto que te escribi\xF3 en las \xFAltimas 24 horas. Fuera de esa ventana la respuesta es 422 con `error` = `window_closed`; para retomar la conversaci\xF3n env\xEDa una plantilla aprobada, que no tiene esta restricci\xF3n.\n- **Antibloqueo:** en las \xFAltimas 24 horas, un mismo contenido se puede enviar como m\xE1ximo 2 veces al mismo contacto (el siguiente responde 422 con `error` = `antiblock_duplicate_content`), y como m\xE1ximo 5 mensajes seguidos sin que el contacto responda (el siguiente responde 422 con `error` = `antiblock_unanswered_limit`). Cuando el contacto responde, los contadores se reinician. Aplica a mensajes y a plantillas, no a reacciones.\n- **Destinatario:** `recipient_mismatch` (422) indica que el tel\xE9fono o el identificador de WhatsApp que mandaste en el cuerpo no corresponde al contacto de la ruta (s\xF3lo las operaciones que reciben ese dato en el cuerpo). `recipient_unsupported` (422) indica que el contacto es un grupo, una lista de difusi\xF3n o un canal: por la API s\xF3lo se escribe a personas individuales.\n- **Estados de entrega:** el evento de webhook `message.status_changed` (enviado, entregado, le\xEDdo, fallido) no se incluye al suscribirte a `*`: tienes que pedirlo de forma expl\xEDcita en `events` al crear o editar la suscripci\xF3n.\n- **Resultado de tus env\xEDos:** cuando env\xEDas con una clave, la llamada no espera al proveedor: la respuesta trae el mensaje ya registrado y en cola, el mismo objeto que devuelve `GET /contacts/{contact}/messages/{message}`, y el env\xEDo sale enseguida despu\xE9s.\n - **En cola no es enviado:** `status` `queued` solo dice que registramos el mensaje y que est\xE1 por salir. El resultado llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`. Un mensaje `queued` no se reintenta: reenviarlo lo duplicar\xEDa.\n - **Aceptado no es entregado:** `status` `sent` solo dice que el proveedor acept\xF3 el mensaje. El estado final (entregado, le\xEDdo o fallido) llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`.\n - **`failed`:** el proveedor rechaz\xF3 el mensaje dando un motivo, o Funnelchat lo fren\xF3 antes de enviarlo. No sali\xF3: reintentar es un intento nuevo y no lo duplica. Cuando lo decidi\xF3 Funnelchat, `error.reason` trae el motivo, `error.code` es `null` y `error.title` es un texto llano en tu idioma; si lo rechaz\xF3 el proveedor, `error.reason` es `null`. Qu\xE9 hacer seg\xFAn `error.reason`: `device_unavailable`, reconecta el n\xFAmero en Funnelchat y env\xEDa de nuevo; `insufficient_balance`, recarga tu billetera de WhatsApp; `window_closed`, env\xEDa una plantilla aprobada; `recipient_unreachable`, revisa el tel\xE9fono del contacto; `template_not_found` o `template_unresolved_placeholder`, corrige la plantilla o sus variables; `not_queued` o `not_sent`, vuelve a intentarlo. Un mensaje enviado con clave que termina `failed` tambi\xE9n llega por `message.status_changed`, con `previous_status` en `null` y su `client_reference`; un `status` `null` no recibe aviso.\n - **`status` `null`:** el proveedor no confirm\xF3 ni rechaz\xF3 el env\xEDo (no respondi\xF3, respondi\xF3 con una falla sin motivo o el env\xEDo se interrumpi\xF3). Puede haber llegado al contacto, no recibir\xE1 avisos de estado y reintentarlo puede duplicarlo.\n - **M\xE1s de una hora en `queued`:** un mensaje cuyo env\xEDo no empez\xF3 dentro de la hora ya no sale. Pasada la hora sin resultado, su `status` es `null`, o `failed` con `error.reason` `not_sent` y su aviso `message.status_changed` si su env\xEDo llega tarde. Reintentar es un env\xEDo nuevo, que s\xF3lo puede duplicar si el env\xEDo hab\xEDa empezado y se cort\xF3.\n - **Error 5xx o llamada sin respuesta:** busca tu `client_reference` en el historial del contacto antes de reintentar, y hazlo al menos un minuto despu\xE9s de la llamada. Si aparece, el mensaje qued\xF3 registrado: no lo reenv\xEDes, su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`; la excepci\xF3n son las tarjetas y los destinos de un reenv\xEDo tras un 5xx: si todos los que aparecen vienen `failed` con `not_queued`, se reintenta la llamada completa; si alguno aparece `queued`, qued\xF3 registrado: no lo reenv\xEDes. Si no aparece, no sali\xF3 nada y puedes reintentar. Repetir una referencia no evita duplicados: dos llamadas con la misma `client_reference` registran dos mensajes.\n - **Error 404 en un env\xEDo:** el contacto no existe. En un reenv\xEDo, tambi\xE9n cuando el mensaje de origen no existe o no es de ese contacto. Al enviar por `phone` o `whatsapp_id`, cuando ese contacto es de otro workspace de tu cuenta.\n - **`POST /contacts/{contact}/message`:** la respuesta trae el mensaje en `message`: `queued` si es un mensaje, o el mensaje registrado de una nota.\n - **`POST /contacts/{contact}/message/contact-card`:** la respuesta trae `messages`: un mensaje por tarjeta, en el orden de `contact_card_ids`, cada uno con su `id` y en cola (`queued`). Todas salen despu\xE9s, desde la cola: el resultado de cada una llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Si una tarjeta no pudo encolarse, su entrada viene `failed` con `error.reason` `not_queued`: no sali\xF3 y volver a enviar esa tarjeta es un intento nuevo. Si alguna no pudo registrarse, la llamada responde un error 5xx y ninguna tarjeta sali\xF3: las que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todas las que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguna aparece `queued`, qued\xF3 registrada: no la reenv\xEDes.\n - **Reenv\xEDo:** la respuesta trae un elemento por destino, en el orden de `targets`, y cada destino que qued\xF3 en cola trae su `message` con su `id` y en cola (`queued`). Todos salen despu\xE9s, desde la cola: el resultado de cada uno llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Un destino que no pudo encolarse viene `blocked` con `reason` `not_sent` y su `message` `failed` con `error.reason` `not_queued`: no sali\xF3 y volver a reenviarlo a ese destino es un intento nuevo. Un destino `blocked` por MAC (`mac_limit_reached` o `mac_lockout_24h`) o con `reason` `target_not_found` (no existe o no es de tu cuenta), `contact_blocked` (bloqueaste al contacto en ese n\xFAmero) o `device_unavailable` trae `message` `null`: no sali\xF3 nada. Si alg\xFAn destino no pudo registrarse, la llamada responde un error 5xx y ninguno sali\xF3: los que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todos los que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguno aparece `queued`, qued\xF3 registrado: no lo reenv\xEDes.\n - **`POST /contacts/{contact}/message/form`:** la respuesta trae solo `message`: el mensaje del formulario en cola (`queued`). Si no pudo ponerse en cola, la respuesta sigue siendo 200 con el mensaje en estado `failed` y `error.reason` `not_queued`: no sali\xF3, as\xED que reintentar no lo duplica. Su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, y la respuesta del contacto por `whatsapp_form.completed`. Si tu cuenta est\xE1 en bloqueo por el l\xEDmite de contactos activos responde 429 `mac_lockout_24h` y no guarda ning\xFAn mensaje ni `client_reference`. Sus errores propios est\xE1n en la gu\xEDa \xABFormularios de WhatsApp\xBB.\n - **`POST /message/send-message` y `POST /devices/{device}/templates/{template_id}/send-template`:** la respuesta trae solo `message`: el mensaje en cola (`queued`), o el mensaje registrado de una nota en `send-message`. Si una variable de la plantilla se completa con un campo personalizado que no existe en tu cuenta, `send-template` responde 422 con `error` `template_unresolved_placeholder`, `placeholders` (hasta 20) y `count` (el total), y no registra ni env\xEDa nada; si la plantilla no existe en tu cuenta, responde 404 `template_not_found`.\n- **Correlacionar tus env\xEDos:** manda `client_reference` en el cuerpo (texto opcional de 1 a 64 caracteres visibles, sin espacios) para identificar cada env\xEDo con tu propia referencia. La misma referencia vuelve en el historial de mensajes del contacto y en el evento `message.status_changed`, de modo que puedes relacionar cada aviso de estado con tu env\xEDo tambi\xE9n cuando la llamada de env\xEDo no respondi\xF3. La referencia no es \xFAnica (dos env\xEDos pueden usar el mismo valor) y no est\xE1 autenticada: la ven todas las claves y todas las suscripciones de la cuenta. Por eso relaci\xF3nala siempre junto con `message_id` y `contact_id`, y tr\xE1tala como texto no confiable cuando la muestres. No pongas datos personales en la referencia. Una llamada que frena una regla de protecci\xF3n de esta secci\xF3n (ritmo, ventana de 24 horas, antibloqueo o destinatario) no registra ning\xFAn mensaje ni referencia. Un rechazo por MAC (`mac_limit_reached` o `mac_lockout_24h`) es distinto: guarda un mensaje con estado de error que lleva la referencia; el env\xEDo de formularios (`POST /contacts/{contact}/message/form`) es la excepci\xF3n: con `mac_lockout_24h` responde 429 y no guarda ning\xFAn mensaje ni referencia. Un mensaje enviado con clave que Funnelchat frena antes de que salga (por ejemplo, por saldo insuficiente) aparece en el historial con estado de error y su referencia, y recibe un aviso `failed` con esa referencia; uno enviado desde el panel no recibe avisos de estado. Las reacciones no aceptan `client_reference`.\n\n## Le\xEDdo y escribiendo\n\nPuedes marcar como le\xEDdo un mensaje que te mand\xF3 un contacto y mostrarle \xABescribiendo\u2026\xBB mientras preparas la respuesta. Ninguna de las dos operaciones env\xEDa un mensaje: no cuentan para el ritmo de env\xEDo ni para las dem\xE1s reglas de \xABEnv\xEDo de mensajes con clave\xBB, y requieren el permiso **Contactos y conversaciones \u2192 Enviar** porque el contacto ve su efecto.\n\n- **Tipos de n\xFAmero:** \xABmarcar como le\xEDdo\xBB funciona en n\xFAmeros de WhatsApp Cloud (API oficial, tambi\xE9n el lado oficial de un n\xFAmero en coexistencia) y en n\xFAmeros conectados por c\xF3digo QR que lo permiten; \xABescribiendo\u2026\xBB funciona solo en n\xFAmeros de WhatsApp Cloud. Si el n\xFAmero no lo permite, la respuesta es 422 con `error` = `provider_unsupported`.\n- **Marcar como le\xEDdo** (`POST /contacts/{contact}/messages/{message}/read`): el contacto ve el doble check azul. WhatsApp marca tambi\xE9n los mensajes anteriores de esa conversaci\xF3n y solo acepta mensajes recibidos en los \xFAltimos 30 d\xEDas. En Funnelchat la conversaci\xF3n queda le\xEDda, igual que cuando un agente la abre en el panel; si llegaron mensajes despu\xE9s del que marcaste, siguen contando como no le\xEDdos.\n- **\xABEscribiendo\u2026\xBB** (`POST /contacts/{contact}/typing`): se muestra sobre el \xFAltimo mensaje que el contacto te mand\xF3 en las \xFAltimas 24 horas, por el n\xFAmero que indiques en `device_id` o, si no lo mandas, por el n\xFAmero de su \xFAltimo mensaje. Dura hasta que respondes o 25 segundos, lo que pase primero. WhatsApp tambi\xE9n marca ese mensaje (y los anteriores) como le\xEDdo, as\xED que el contacto ve el doble check azul; en Funnelchat la conversaci\xF3n no cambia. P\xEDdelo solo cuando vayas a responder.\n- **L\xEDmites:** entre las dos operaciones, hasta 20 llamadas por segundo por n\xFAmero y 50 por cuenta. El l\xEDmite por cuenta incluye las llamadas rechazadas con 429 o con alguno de los c\xF3digos 422 de esta secci\xF3n; no cuentan los 404 ni los errores de validaci\xF3n del cuerpo (422 con errors). Al superar cualquiera de los dos, la respuesta es 429 con `error` = `rate_limited` y el encabezado `Retry-After`.\n- **Si WhatsApp no confirma:** la respuesta es 502 con `error` = `provider_error` y no se cambia nada; puedes reintentar.\n\n## Enviar un archivo por URL\nNo subes el archivo a Funnelchat: en `file` mandas la direcci\xF3n (URL) del archivo y WhatsApp o el proveedor lo descarga desde ah\xED. Funnelchat no abre la URL al recibir tu llamada, as\xED que un archivo inaccesible, vencido o fuera de los l\xEDmites se conoce al enviarlo: el mensaje termina `failed` con el error del proveedor.\n- **La URL:** `file` admite hasta 4096 caracteres; m\xE1s de eso se rechaza con un 422 en `file`. Si `file` empieza con `http`, en may\xFAsculas o min\xFAsculas, tiene que ser una URL que empiece con `http://` o `https://`; si no (por ejemplo `httpfoto.jpg`), la respuesta es 422 con el error en `file`. Te recomendamos `https://`; `http://` tambi\xE9n se acepta, pero viaja sin cifrar. Debe poder descargarse sin credenciales; puede ser una URL firmada. Tiene que seguir v\xE1lida hasta que el mensaje sale: un env\xEDo con clave puede esperar en cola hasta 54 minutos, as\xED que usa URLs que duren al menos una hora. No pongas secretos en la ruta ni en el dominio de la URL: en los registros de actividad (logs) del env\xEDo que escribe Funnelchat, de la URL quedan s\xF3lo el esquema, el dominio y la ruta (sin el usuario y la contrase\xF1a, la parte de consulta despu\xE9s de `?` ni el fragmento despu\xE9s de `#`) y del resultado s\xF3lo identificadores, el tipo del mensaje y, del error del proveedor, sus c\xF3digos y, s\xF3lo si tu n\xFAmero es de WhatsApp Cloud, su categor\xEDa, su t\xEDtulo corto y su identificador de traza, nunca el texto de la respuesta del proveedor; la ruta y el dominio s\xED quedan. La URL completa viaja a WhatsApp o al proveedor de tu n\xFAmero, que la descarga y puede guardarla en sus propios registros. Adem\xE1s, la URL completa, con su consulta, se guarda en el historial del contacto, la ven los usuarios de tu cuenta y se copia al reenviar el mensaje: usa URLs firmadas de corta duraci\xF3n (al menos el tiempo de la cola, cerca de una hora) y limitadas a un solo archivo.\n- **El tipo:** decide la extensi\xF3n de la ruta de la URL si es una de la lista; si no, el final de la URL completa si es una de la lista. Si la ruta no termina en una extensi\xF3n de la lista y s\xF3lo lo hace el final de la URL completa (por ejemplo `https://cdn.example.com/video.mov?poster=frame.jpg`), el mensaje queda registrado con ese tipo, pero WhatsApp o el proveedor pueden enviarlo seg\xFAn la extensi\xF3n de la ruta; si necesitas un tipo fijo, env\xEDa `file_type`:\n - `.jpg`, `.jpeg`, `.png`: `image`\n - `.webp`: `sticker`\n - `.ogg`, `.oga`, `.mp3`, `.m4a`, `.aac`, `.opus`, `.wav`: `audio`\n - `.gif`, `.mp4`: `video`\n - `.doc`, `.docx`, `.pdf`: `document`\n- **Cu\xE1ndo env\xEDas `file_type`:** si la URL no termina en una extensi\xF3n reconocida (por ejemplo una URL firmada sin extensi\xF3n), la respuesta es 422 con el error en `file_type`: agrega `file_type` con uno de estos valores: `image`, `video`, `audio`, `document`, `sticker`. Cuando lo env\xEDas, manda sobre la extensi\xF3n: sirve tambi\xE9n para un archivo cuya extensi\xF3n no es de los tipos de la lista, como `.xlsx`, que se env\xEDa con `file_type` = `document` y `file_name` (por ejemplo `reporte.xlsx`), porque un documento cuya URL no termina en una extensi\xF3n de la lista necesita su nombre. Si la URL termina en una extensi\xF3n que no es la del archivo (como `/descarga.php?id=7`) y no env\xEDas `file_type`, la respuesta es 415 con la extensi\xF3n recibida (`php`); con `file_type` se env\xEDa y, si es un documento, tambi\xE9n hace falta `file_name`.\n- **Extensi\xF3n no soportada:** si la ruta de la URL termina en un formato real que no enviamos solos (por ejemplo `.xlsx` o `.zip`), la URL completa no termina en una extensi\xF3n de la lista y no mandas `file_type`, la respuesta es 415 con la extensi\xF3n recibida.\n- **El nombre del archivo:** `file_name` (opcional, hasta 255 caracteres, sin `/`, `\\`, `?`, `#` ni caracteres de control) es el nombre con el que el contacto ve un documento; ll\xE9valo siempre con su extensi\xF3n, por ejemplo `factura.pdf`. Es obligatorio cuando env\xEDas un documento por URL y la URL no termina en una extensi\xF3n de la lista: sin \xE9l la respuesta es 422 con el error en `file_name`.\n- **Formatos y tama\xF1os:** los formatos aceptados por WhatsApp y el tama\xF1o m\xE1ximo de cada tipo son:\n - `image`: image/jpeg, image/png, image/webp; hasta 5 MB.\n - `video`: video/mp4, video/3gpp, image/gif; hasta 16 MB.\n - `audio`: audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, audio/opus; hasta 16 MB.\n - `document`: text/plain, application/pdf, application/vnd.ms-powerpoint, application/msword, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; hasta 100 MB.\n - `sticker`: image/webp; hasta 100 KB si es est\xE1tico y 500 KB si es animado.\n- **Historial y reenv\xEDo:** el historial del contacto guarda la URL tal cual la enviaste, y el nombre `file_name` no se guarda. Al reenviar un mensaje cuyo archivo es una URL, se reenv\xEDa la misma URL; con una extensi\xF3n de la lista, como esa extensi\xF3n indica; si no, un mensaje que enviaste se reenv\xEDa con su tipo s\xF3lo cuando su extensi\xF3n es un formato que WhatsApp acepta para ese tipo (los de \xABFormatos y tama\xF1os\xBB, por ejemplo `.3gp` para `video` o `.amr` para `audio`). Una URL sin extensi\xF3n, un formato no aceptado, un documento cuya URL no termina en una extensi\xF3n de la lista (por ejemplo `/descarga.php?id=7`) o un mensaje recibido sin una extensi\xF3n de la lista no se puede reenviar: la respuesta es 422 con `error` = `not_forwardable` y `reason` = `unsupported_file_extension`.\n\n## Formularios de WhatsApp\n\nUn formulario de WhatsApp es un conjunto de pantallas con preguntas que tu contacto abre y completa dentro del chat. Con la API puedes armarlo, publicarlo, enviarlo a un contacto y recibir sus respuestas. S\xF3lo funciona con n\xFAmeros de WhatsApp Cloud (API oficial) de tu cuenta, y s\xF3lo se puede enviar dentro de las 24 horas siguientes al \xFAltimo mensaje del contacto a ese n\xFAmero. Los n\xFAmeros conectados por c\xF3digo QR no sirven para formularios.\n- **Identificadores:** un formulario tiene un identificador estable y una versi\xF3n por cada vez que lo cambias. No los confundas:\n\n | Qu\xE9 es | D\xF3nde aparece |\n |---|---|\n | **Formulario** (estable, el mismo en todas sus versiones) | `family_id` en las respuestas de formularios, `{familyId}` en las rutas `/forms/families/{familyId}/\u2026`, `form_id` al enviar y `data.form.id` en `whatsapp_form.completed` |\n | **Versi\xF3n** (cambia con cada versi\xF3n nueva) | `id` en las respuestas de formularios, `{id}` en las rutas `/forms/{id}/\u2026` de edici\xF3n, publicaci\xF3n y n\xFAmeros, y `data.form.version_id` en `whatsapp_form.completed` |\n\n Al enviar pides el formulario, no la versi\xF3n: sale la versi\xF3n publicada vigente. Si publicas una versi\xF3n nueva, lo que se env\xEDa cambia sin que cambies tu integraci\xF3n.\n- **Recorrido, paso a paso:**\n 1. **Crea el formulario:** `POST /forms` con `template_key` `blank` (un formulario para armar desde cero; `GET /forms/templates` lista las dem\xE1s plantillas), `device_id` (el n\xFAmero de WhatsApp Cloud de tu cuenta) y, si quieres, `device_ids` con otros n\xFAmeros. Responde `201` con la versi\xF3n en borrador (`id`), su `family_id`, `lint` y `numbers`. Un formulario no se crea mandando su contenido: el contenido va en el paso siguiente. Ojo con `blank`: no viene vac\xEDo, trae una pantalla (`SCREEN_BLNK`) sin elementos cuyo `next` es `complete`, es decir, que termina el formulario. Si en el `PUT` conservas esa pantalla y agregas otra, la segunda queda inalcanzable y `lint` marca `screen_unreachable` (error que impide publicar) hasta que el `next` de la primera apunte a la segunda (o `auto`, que sigue el orden). Lo m\xE1s simple es reemplazar `screens` entera con tus propias pantallas.\n 2. **Arma la definici\xF3n:** `PUT /forms/{id}` con `definition` (`screens`, cada una con `id`, `title`, `cta`, `next` e `items`) y, si quieres, `name` y `expected_updated_at`. El servidor guarda la definici\xF3n tal como llega y la respuesta trae `lint` junto a `data`: una lista de `{code, severity, screenId, itemId, message}` con lo que falta corregir. Lo incompleto se guarda igual; los errores (`severity` `error`) impiden publicar y los avisos (`aviso`) no. Una definici\xF3n con la forma rota responde `422`. Para leerla de nuevo, `GET /forms/{id}` trae la versi\xF3n con su definici\xF3n y `GET /forms/families/{familyId}/definition` la del formulario: la de la versi\xF3n publicada vigente o, si todav\xEDa no hay ninguna, la de la m\xE1s nueva. Con `expected_updated_at` evitas pisar un cambio m\xE1s nuevo: si no es el \xFAltimo guardado responde `409` `STALE`.\n 3. **Valida en Meta (opcional):** `POST /forms/{id}/sync` corre el linter, sube el formulario a Meta y trae sus observaciones en `lint` con `code` `meta`, sin publicar. Si la definici\xF3n tiene errores responde `422` `LINT`, igual que al publicar.\n 4. **Publica:** `POST /forms/{id}/publish` hace la validaci\xF3n y publica; es inmediato. Si la definici\xF3n tiene errores responde `422` `LINT` con la lista, y si Meta la rechaza, `502` `META` (`422` `META` si la rechaza porque tu cuenta de WhatsApp Business no cumple sus requisitos de integridad). Un formulario sin ninguna pregunta no se puede publicar. Los otros n\xFAmeros del formulario se publican en segundo plano: consulta su estado (`draft`, `publishing`, `published` o `failed`) en `GET /forms/{id}/numbers`.\n 5. **Suma o quita n\xFAmeros (opcional):** `POST /forms/{id}/numbers` con `device_id` publica el formulario en otro n\xFAmero de WhatsApp Cloud de tu cuenta, `POST /forms/{id}/numbers/retry` reintenta los que fallaron y `DELETE /forms/{id}/numbers/{deviceId}` quita uno. El n\xFAmero con el que creaste el formulario no se quita: `422` `PRIMARY_NUMBER`. Un n\xFAmero que no es de tu cuenta o no es WhatsApp Cloud responde `422` `NUMBER_NOT_ELIGIBLE`.\n 6. **Env\xEDa:** `POST /contacts/{contact}/message/form` con `form_id` (el formulario, no la versi\xF3n) y `device_id`; opcionalmente `text` (hasta 1024 caracteres), `button_text` (hasta 30, sin emojis) y `client_reference`. S\xF3lo se env\xEDa con una clave de API. La clave de un asiento s\xF3lo env\xEDa a los contactos asignados a ese asiento (con cualquier otro contacto responde el `404` de un contacto que no existe); la clave del titular env\xEDa a cualquier contacto de la cuenta. Responde `200` con `message` en cola (`queued`); sus errores propios est\xE1n al final de esta gu\xEDa.\n 7. **Recibe las respuestas:** suscr\xEDbete al evento `whatsapp_form.completed` (tambi\xE9n llega si te suscribes a `*`) o lee `GET /forms/families/{familyId}/responses` y `GET /forms/families/{familyId}/responses/{runId}`. `GET /forms/families/{familyId}/metrics` trae los env\xEDos, aperturas y respuestas.\n - **Cambiar un formulario publicado:** un formulario publicado no se edita (`PUT` y `DELETE` responden `409` `PUBLISHED`). Crea una versi\xF3n nueva con `POST /forms/{id}/versions`, que nace como borrador (`201`, o `200` con `created: false` si ya hab\xEDa un borrador), ed\xEDtala y publ\xEDcala. El `form_id` no cambia: el env\xEDo usa la versi\xF3n publicada vigente.\n- **Reglas de la definici\xF3n:** el servidor no genera ni corrige identificadores: quien arma la definici\xF3n los genera, y deben ser v\xE1lidos y \xFAnicos. Meta acepta s\xF3lo letras y guiones bajos.\n - **Pantallas:** `id` con `SCREEN_` m\xE1s letras (por ejemplo `SCREEN_ABCD`), sin d\xEDgitos, y \xFAnicos en el formulario. `next` es `auto` (la siguiente), `complete` (termina) o el `id` de una pantalla posterior: s\xF3lo se salta hacia adelante.\n - **Preguntas:** `name` en min\xFAsculas y guiones bajos (`a-z` y `_`), \xFAnico en todo el formulario. Sin d\xEDgitos, tildes, `\xF1` ni espacios. No uses los nombres reservados `flow_token`, `version` ni `extension_message_response`. No cambies un `name` despu\xE9s de crearlo: las respuestas, las ramas y las etiquetas lo usan.\n - **Opciones:** `id` de cada opci\xF3n en min\xFAsculas y guiones bajos, \xFAnico en su pregunta (por ejemplo `op_comercio`).\n - **Qu\xE9 marca el linter:** `field_name_reserved` (nombre reservado), `field_name_invalid` (nombre fuera de letras y guiones bajos) y `option_id_invalid` (id de opci\xF3n fuera de ese alfabeto) son errores que impiden validar y publicar. El linter no revisa que los identificadores sean \xFAnicos ni que existan los campos personalizados y las etiquetas que referencias: compru\xE9balo t\xFA.\n - **Campos personalizados y etiquetas:** si una pregunta guarda su respuesta en un campo personalizado (`map` con `target` `custom` y `key` con el nombre del campo) o asigna etiquetas (`tagMap`, con el nombre de la etiqueta), ese campo y esa etiqueta tienen que existir en tu cuenta. El formulario no los crea: si no existen, la respuesta se guarda en el formulario pero no se escribe en el contacto ni se le asigna la etiqueta. Un campo num\xE9rico s\xF3lo admite preguntas `number` y uno de fecha s\xF3lo preguntas `date`; el servidor no lo revisa.\n - **No se pueden publicar** los formularios que necesitan un servidor propio (`endpoint` en `true`, opciones en vivo): el linter los marca como error.\n- **L\xEDmites del formulario:** hasta 5 pantallas, 50 elementos por pantalla, 3 im\xE1genes por pantalla y 2 enlaces por pantalla; pasarse es un error que impide publicar. El nombre del formulario admite hasta 120 caracteres. Las im\xE1genes (`type` `image`, con `src` en base64 sin el prefijo `data:`) deben ser JPG o PNG, de hasta 300 KB cada una y 4 MB entre todas las del formulario; el servidor no revisa su tama\xF1o ni su formato, as\xED que quien arma la definici\xF3n debe respetarlos. El guardado acepta un `src` con el prefijo `data:`, pero se env\xEDa a Meta tal como lo guardaste y Meta dibuja un recuadro roto: manda siempre la carga en base64 pelada, sin prefijo.\n - **Tope de cada guardado:** adem\xE1s, un guardado rechaza (`422`) una definici\xF3n con m\xE1s de 20 pantallas, 100 elementos en una pantalla o 200 opciones en una pregunta. Esos topes quedan por encima de los l\xEDmites de arriba. Adem\xE1s, cada guardado env\xEDa la definici\xF3n entera, im\xE1genes incluidas, y debe pesar menos de unos 6 MB: pasado ese tama\xF1o la plataforma rechaza la llamada antes de que llegue al formulario, as\xED que no cuentes con m\xE1s.\n- **Qu\xE9 puede hacer cada clave:** los permisos se heredan hacia abajo: **Contactos y conversaciones \u2192 Enviar** incluye **Contactos y conversaciones \u2192 Editar**, que incluye **Contactos y conversaciones \u2192 Ver**. Con **Contactos y conversaciones \u2192 Ver** lees formularios, su definici\xF3n (`GET /forms/families/{familyId}/definition`), versiones, n\xFAmeros, respuestas y m\xE9tricas; con **Contactos y conversaciones \u2192 Editar** adem\xE1s armas, guardas, publicas, validas, creas versiones, eliminas borradores y sumas, reintentas o quitas n\xFAmeros; con **Contactos y conversaciones \u2192 Enviar** adem\xE1s env\xEDas. Por eso una clave con **Contactos y conversaciones \u2192 Enviar**, aunque s\xF3lo la hayas pensado para enviar, tambi\xE9n puede armar, publicar y quitar n\xFAmeros de formularios: dale ese permiso s\xF3lo a quien conf\xEDas para eso. Las operaciones `summary`, `analysis` y `deliveries` del panel no est\xE1n abiertas a claves.\n - **Topes de llamadas por clave:** cada clave puede hacer hasta 120 lecturas por minuto y hasta 60 escrituras por minuto en formularios (cada clave lleva su propio contador: dos claves de la misma cuenta, sea del titular o de un asiento, no se frenan entre s\xED, y ninguna gasta el tope del panel). Al superarlos la respuesta es `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar. Validar, publicar y tocar n\xFAmeros tienen adem\xE1s el tope de 30 por minuto compartido por toda la cuenta, que se explica en los errores al final de esta gu\xEDa.\n - **Cuenta:** la clave de un asiento (un agente) opera sobre los formularios del titular de la cuenta, no sobre formularios propios del asiento.\n - **Respuestas con clave de un asiento:** `responses` y `responses/{runId}` se limitan a los contactos que ese asiento puede ver en Funnelchat; las m\xE9tricas (`metrics`) cuentan todas las respuestas del formulario.\n - **Detalle de una respuesta con clave:** `GET /forms/families/{familyId}/responses/{runId}` no trae `last_error` en sus efectos; el resto de cada efecto es igual al del panel.\n- **Aviso `whatsapp_form.completed`:** `data.form` trae `id` (el formulario), `name` y `version_id` (la versi\xF3n que recibi\xF3 el contacto); `message_id` es el mensaje del formulario y puede ser `null`; `answers` es una lista de `{key, label, value}`, una por pregunta y en el orden del formulario (`key` es el `name` de la pregunta, y `value` es `null` si no la contest\xF3). El mismo aviso puede llegar m\xE1s de una vez con el mismo `id`: deduplica por `id`. Cada respuesta de texto llega con a lo sumo 600 caracteres. Si el contacto vuelve a enviar el mismo formulario no se genera otro aviso: queda la primera respuesta. Los detalles y el ejemplo est\xE1n en la secci\xF3n \xABWebhooks\xBB.\n- **Errores al armar y publicar:** `404` si el `id` no existe o no es de tu cuenta (un id de otra cuenta responde igual); `409` `STALE` (el guardado no es el \xFAltimo) y `409` `PUBLISHED` (la versi\xF3n ya est\xE1 publicada); `422` `LINT` al publicar con errores en la definici\xF3n, `422` `NUMBER_NOT_ELIGIBLE` y `422` `PRIMARY_NUMBER` en los n\xFAmeros; `502` `META` si Meta rechaza la operaci\xF3n o no responde, y `422` `META` si la rechaza porque tu cuenta de WhatsApp Business no cumple sus requisitos de integridad. Validar, publicar y tocar n\xFAmeros (`sync`, `publish` y las operaciones de `numbers`) sube a Meta, que limita por cuenta de WhatsApp Business: todas las claves de tu cuenta, la del titular y las de los asientos, comparten un tope de 30 de estas llamadas por minuto. Al superarlo responde `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar.\n- **Errores al enviar un formulario:** adem\xE1s de los errores de las reglas de protecci\xF3n de esta documentaci\xF3n, `POST /contacts/{contact}/message/form` responde: `404` si el contacto no existe, no es de tu cuenta o, con la clave de un asiento, no est\xE1 asignado a ese asiento; `404` con `form_not_found` si el formulario no existe en tu cuenta; `422` con `device_unavailable` (el n\xFAmero no es de tu cuenta o no est\xE1 conectado), `number_not_eligible` (no es un n\xFAmero de WhatsApp Cloud que pueda enviar formularios), `form_not_published` (el formulario no tiene versi\xF3n publicada: publ\xEDcalo), `form_not_on_number` (no est\xE1 publicado en ese n\xFAmero: s\xFAmalo con `POST /forms/{id}/numbers` y espera a que se publique), `recipient_unreachable` (el contacto no tiene tel\xE9fono ni identificador de WhatsApp al que ese n\xFAmero pueda escribir), `contact_blocked` (bloqueaste al contacto en ese n\xFAmero: desbloqu\xE9alo para escribirle) y `window_closed` (pasaron m\xE1s de 24 horas desde el \xFAltimo mensaje del contacto); `429` con `rate_limited` y `Retry-After` si superas el ritmo; y `422` con `not_sent` (el destinatario que se registr\xF3 no coincide con el contacto verificado, o el mensaje no qued\xF3 como un env\xEDo de la conversaci\xF3n: no se registr\xF3 ni sali\xF3 nada y puedes reintentar; son las \xFAnicas dos situaciones y en el uso normal no ocurren). Si Funnelchat no pudo poner el mensaje en cola, la respuesta no es un error: es `200` con el mensaje en estado `failed` y `error.reason` `not_queued`; no sali\xF3, as\xED que reintentar no lo duplica. Si tu cuenta est\xE1 bloqueada por el l\xEDmite de contactos activos, el env\xEDo responde `429` con `mac_lockout_24h`, como los dem\xE1s env\xEDos con clave, pero a diferencia de ellos no guarda ning\xFAn mensaje ni `client_reference`: no queda nada en el historial del contacto ni se puede buscar por la referencia, as\xED que reintenta cuando termine el bloqueo. Un `500` indica que el mensaje no pudo registrarse. Si la llamada no responde o da un error 5xx, busca tu `client_reference` en el historial del contacto antes de reintentar.'
|
|
51333
51493
|
},
|
|
51334
51494
|
{
|
|
51335
51495
|
domain: "communities",
|
|
@@ -51576,11 +51736,11 @@ var openapi_default = {
|
|
|
51576
51736
|
generated_by: "scripts/generate.mjs",
|
|
51577
51737
|
note: "GENERATED: do not edit it. The portal's snapshot plus the pending layers.",
|
|
51578
51738
|
inputs: {
|
|
51579
|
-
"spec/funnelchat.json": "
|
|
51739
|
+
"spec/funnelchat.json": "38ae497ab7e6089b31b589e1f93c2289524348d581796eba109568885642813c",
|
|
51580
51740
|
"spec/pending.json": "051680bd82944c0e248d20d1499d89e61a231c6ba3ef6a5d864cf18a97aae1ad",
|
|
51581
51741
|
"spec/pending-sandbox.json": "e75e9a7b63c5913c11fc35fa06d23bd32a697832a4da105c377eac9a8f4e62a3"
|
|
51582
51742
|
},
|
|
51583
|
-
snapshot_commit: "
|
|
51743
|
+
snapshot_commit: "51c0aeb",
|
|
51584
51744
|
pending_commit: "a8f83509d4a40d6116a9c0487bf0aee639948273",
|
|
51585
51745
|
pending_applied: [
|
|
51586
51746
|
"POST /api/v1/webhooks/{id}/rotate-secret [pending.json]",
|
|
@@ -52603,7 +52763,7 @@ function resolveFile(body2, style) {
|
|
|
52603
52763
|
|
|
52604
52764
|
// package.json
|
|
52605
52765
|
var package_default = {
|
|
52606
|
-
version: "0.3.
|
|
52766
|
+
version: "0.3.3"};
|
|
52607
52767
|
|
|
52608
52768
|
// src/version.ts
|
|
52609
52769
|
var VERSION = package_default.version;
|
|
@@ -54970,6 +55130,21 @@ var messageHandlers = {
|
|
|
54970
55130
|
const template = getTemplate(ctx, ctx.path.template);
|
|
54971
55131
|
return ok({ data: (template.buttons ?? []).filter((b) => b.type === "QUICK_REPLY").map((b) => ({ text: b.text ?? "" })) });
|
|
54972
55132
|
},
|
|
55133
|
+
// Bring the templates the number has in Meta (conversations v1.19.37 `MessageTemplateController::sync`). Nothing goes
|
|
55134
|
+
// to Meta: an official number answers that it finished (its templates are already the sandbox's), a QR one that it
|
|
55135
|
+
// has none, and a number outside the account or the key's workspace, conversations' 404.
|
|
55136
|
+
"POST /api/v1/devices/{device}/templates": (ctx) => {
|
|
55137
|
+
const device = ctx.app.store.devices.get(Number(ctx.path.device));
|
|
55138
|
+
if (!device || !sees(ctx.workspace, device.workspaceId)) throw objectNotFound();
|
|
55139
|
+
if (device.kind !== "cloud") {
|
|
55140
|
+
throw new ApiError(
|
|
55141
|
+
422,
|
|
55142
|
+
"number_not_connected_to_meta",
|
|
55143
|
+
"Este n\xFAmero no tiene plantillas de Meta que sincronizar: est\xE1 conectado por QR o no tiene una cuenta de WhatsApp Business o credenciales v\xE1lidas en Meta."
|
|
55144
|
+
);
|
|
55145
|
+
}
|
|
55146
|
+
return { status: 202, body: { status: "completed" } };
|
|
55147
|
+
},
|
|
54973
55148
|
"DELETE /api/v1/devices/{device}/templates/{template}": (ctx) => {
|
|
54974
55149
|
const template = getTemplate(ctx, ctx.path.template);
|
|
54975
55150
|
if (template.deviceId !== Number(ctx.path.device)) throw notFound("La plantilla no es de este n\xFAmero.");
|
|
@@ -57077,5 +57252,5 @@ function createSandbox(options = {}) {
|
|
|
57077
57252
|
}
|
|
57078
57253
|
|
|
57079
57254
|
export { ConfigError, DEFAULT_CONFIG_FILE, DEFAULT_PORT, DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, parseArgs, resolveConfig, signature };
|
|
57080
|
-
//# sourceMappingURL=chunk-
|
|
57081
|
-
//# sourceMappingURL=chunk-
|
|
57255
|
+
//# sourceMappingURL=chunk-EQLGOYR6.js.map
|
|
57256
|
+
//# sourceMappingURL=chunk-EQLGOYR6.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ConfigError, parseArgs, VERSION, DEFAULT_PORT, DEFAULT_CONFIG_FILE, resolveConfig, createSandbox } from './chunk-
|
|
2
|
+
import { ConfigError, parseArgs, VERSION, DEFAULT_PORT, DEFAULT_CONFIG_FILE, resolveConfig, createSandbox } from './chunk-EQLGOYR6.js';
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
var HELP = `funnelchat-sandbox ${VERSION}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature } from './chunk-
|
|
1
|
+
export { DELIVERY_HEADER, EDGE_ERRORS, EVENT_HEADER, FAILURE_REASONS, FORM_TEMPLATES, META_ERRORS, REJECTIONS, REQUEST_ID_HEADER, SEND_FILE_TYPES, SIGNATURE_HEADER, SUPPORTED_EXTENSIONS, VERSION, catalog, createSandbox, defaultConfig, mergeConfig, signature } from './chunk-EQLGOYR6.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
package/spec/procedencia.json
CHANGED
|
@@ -2,46 +2,46 @@
|
|
|
2
2
|
"description": "Where each file of spec/ came from. Written by `npm run generate -- --spec <file>` or `-- --pending <file>`; do not edit it by hand. spec/pending-sandbox.json belongs to this repo: its sources are inside it.",
|
|
3
3
|
"snapshot": {
|
|
4
4
|
"file": "spec/funnelchat.json",
|
|
5
|
-
"sha256": "
|
|
6
|
-
"bytes":
|
|
5
|
+
"sha256": "38ae497ab7e6089b31b589e1f93c2289524348d581796eba109568885642813c",
|
|
6
|
+
"bytes": 1228756,
|
|
7
7
|
"source": {
|
|
8
8
|
"repository": "git@github.com:Funnelchat20/developers-portal.git",
|
|
9
9
|
"path": "generated/specs/funnelchat-completa.json",
|
|
10
|
-
"branch": "
|
|
11
|
-
"commit": "
|
|
10
|
+
"branch": "feat/plantillas-de-meta",
|
|
11
|
+
"commit": "51c0aeb",
|
|
12
12
|
"commit_date": null,
|
|
13
13
|
"file_tracked_in_git": false,
|
|
14
14
|
"tracked_files_clean": null,
|
|
15
|
-
"file_modified_at": "2026-11-
|
|
16
|
-
"note": "Built by `npm run build` of developers-portal
|
|
15
|
+
"file_modified_at": "2026-11-06T12:43:27.931Z",
|
|
16
|
+
"note": "Built by `npm run build` of developers-portal feat/plantillas-de-meta (v0.6.7, not published yet) on 06-11. Not the published /specs/ file, which carries only the visible operations."
|
|
17
17
|
},
|
|
18
|
-
"imported_at": "2026-11-
|
|
18
|
+
"imported_at": "2026-11-06T12:46:17.581Z",
|
|
19
19
|
"spec": {
|
|
20
20
|
"openapi": "3.1.0",
|
|
21
21
|
"title": "Funnelchat API",
|
|
22
22
|
"version": "v0",
|
|
23
|
-
"paths":
|
|
24
|
-
"operations":
|
|
23
|
+
"paths": 219,
|
|
24
|
+
"operations": 266,
|
|
25
25
|
"webhooks": 29,
|
|
26
26
|
"domains": [
|
|
27
27
|
{
|
|
28
28
|
"domain": "conversations",
|
|
29
|
-
"tag": "v1.19.
|
|
30
|
-
"sha": "
|
|
29
|
+
"tag": "v1.19.37",
|
|
30
|
+
"sha": "a4d0136be",
|
|
31
31
|
"branch": null,
|
|
32
32
|
"snapshot_at": null
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"domain": "communities",
|
|
36
|
-
"tag":
|
|
37
|
-
"sha":
|
|
36
|
+
"tag": null,
|
|
37
|
+
"sha": null,
|
|
38
38
|
"branch": null,
|
|
39
39
|
"snapshot_at": null
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"domain": "accounts",
|
|
43
|
-
"tag":
|
|
44
|
-
"sha":
|
|
43
|
+
"tag": null,
|
|
44
|
+
"sha": null,
|
|
45
45
|
"branch": null,
|
|
46
46
|
"snapshot_at": null
|
|
47
47
|
}
|