---
name: whatsapp-flows
description: >
  Genera Flow JSON válido y listo para usar en WhatsApp Business Platform. Úsalo
  siempre que el usuario pida crear, construir o escribir un WhatsApp Flow, un
  Flow JSON, un formulario multi-pantalla de WhatsApp, o cualquier experiencia
  interactiva dentro de WhatsApp. Trigger on: "crea un flow de whatsapp",
  "flow json", "whatsapp flow", "pantallas de whatsapp", "formulario de
  whatsapp", "flow para whatsapp business", "endpoint para flow", "data_exchange",
  "flow dinámico", o cualquier solicitud de recolectar datos, construir un formulario
  multi-paso, conectar un backend a WhatsApp, o crear una experiencia guiada dentro
  de WhatsApp. Úsalo también cuando el usuario pregunte por qué su data_exchange
  no funciona, por qué el endpoint no responde, o por qué las pantallas quedan vacías.
  Siempre usa este skill antes de escribir cualquier Flow JSON o código de endpoint.
---

# WhatsApp Flows — Flow JSON Builder

Tu trabajo es producir Flow JSON válido y listo para usar, y código de endpoint
funcional cuando el flow es dinámico. El usuario ya tiene el setup de WhatsApp API.
Solo necesita el JSON y/o el endpoint.

**Versiones recomendadas (confirmadas):**
- Flow JSON: `7.3`
- Data API: `4.0`
- Message version: `3`

Versiones congeladas (no se pueden publicar flows nuevos): 2.1, 3.0, 3.1, 4.0, 5.0
Usa `7.3` para todo flow nuevo.

---

## Estructura top-level

```json
{
  "version": "7.3",
  "data_api_version": "4.0",
  "routing_model": {
    "SCREEN_1": ["SCREEN_2"],
    "SCREEN_2": []
  },
  "screens": []
}
```

- `data_api_version`: incluir solo cuando el flow llama a un endpoint
- `routing_model`: requerido con endpoint. Auto-generado para flows estáticos
- `data_channel_uri`: deprecado desde v3.0. Configura el endpoint vía Flows API
- Max 10 rutas por pantalla, **max 100 pantallas** (límite técnico), max 10 MB total
- Recomendación UX: < 8 pantallas para completar en < 5 minutos. El límite de 100 es solo técnico.

---

## Estructura de pantalla

```json
{
  "id": "SCREEN_ID",
  "title": "Título",
  "terminal": false,
  "success": true,
  "refresh_on_back": false,
  "sensitive": ["campo_password"],
  "data": {
    "campo": {
      "type": "string",
      "__example__": "valor de ejemplo"
    }
  },
  "layout": {
    "type": "SingleColumnLayout",
    "children": []
  }
}
```

- `"SUCCESS"` es palabra reservada — nunca usar como ID de pantalla
- `terminal`: marca el estado final. Se permiten múltiples pantallas terminales
- `success`: solo en pantallas terminales. Controla la animación de WhatsApp
- `refresh_on_back`: solo con endpoint. Si `true`, dispara `data_exchange` al presionar atrás
- `sensitive` (v5.1+): lista de campos a enmascarar en el resumen visible al usuario
- Toda pantalla terminal requiere un componente `Footer`
- Todo `${data.campo}` referenciado en la pantalla DEBE estar declarado en `data` con `__example__`

---

## Sintaxis de variables

| Sintaxis | Fuente |
|----------|--------|
| `${data.campo}` | Datos del endpoint o payload de navigate |
| `${form.campo}` | Input actual del usuario en esta pantalla |
| `${screen.SCREEN_ID.data.campo}` | Datos de otra pantalla (v4.0+) |
| `${screen.SCREEN_ID.form.campo}` | Form input de otra pantalla (v4.0+) |

Con referencias globales NO es necesario declarar el campo en el `data` block de la pantalla consumidora.

**Expresiones anidadas (v6.0+):** envuelve con backticks para usar operadores:
- `"visible": "\`${form.edad} >= 18\`"`
- `"text": "\`'Hola ' ${form.nombre}\`"`
- Operadores: `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `!`, `+`, `-`, `/`, `%`

---

## Acciones

### navigate
```json
{
  "name": "navigate",
  "next": { "type": "screen", "name": "SIGUIENTE_PANTALLA" },
  "payload": { "campo": "${form.campo}" }
}
```
No usar en Footer de pantalla terminal.

### complete
```json
{
  "name": "complete",
  "payload": { "dato_enviado": "${form.campo}" }
}
```
Solo válido en pantallas terminales. Dispara webhook `nfm_reply`.

### data_exchange
```json
{ "name": "data_exchange", "payload": { "clave": "${form.campo}" } }
```
Requiere `data_api_version` en el top-level. El endpoint decide la siguiente pantalla.

### update_data (v6.0+)
```json
{ "name": "update_data", "payload": { "nombre_campo": "nuevo_valor" } }
```
Actualiza datos de la pantalla actual sin navegación. Útil para toggles de visibilidad
que no requieren lógica de servidor.

### open_url (v6.0+)
```json
{ "name": "open_url", "url": "https://ejemplo.com" }
```
Solo en `EmbeddedLink` y `OptIn`.

---

## data_exchange — contrato completo

Esta es la sección más crítica. El 90% de los fallos de data_exchange son violaciones
de este contrato.

### Protocolos de cifrado — dos casos distintos

| Caso | Protocolo |
|------|-----------|
| `data_exchange` (payloads JSON) | RSA-OAEP-SHA256 + **AES-128-GCM** |
| Media (PhotoPicker / DocumentPicker) | **AES256-CBC + HMAC-SHA256 + PKCS7** |

No confundirlos. El endpoint de data_exchange usa AES-128-GCM.
El descifrado de archivos de medios usa AES256-CBC. Ver `references/endpoints.md`.

### El ciclo completo

```
Usuario interactúa
  → WhatsApp encripta payload (RSA-OAEP + AES-128-GCM)
  → POST a tu endpoint_uri
  → Tu servidor desencripta
  → Procesa lógica de negocio
  → Construye respuesta con el screen destino + sus datos
  → Re-encripta respuesta (AES-128-GCM con IV invertido)
  → WhatsApp renderiza la siguiente pantalla
```

### Lo que llega a tu endpoint (ya desencriptado)

```json
{
  "version": "3.0",
  "action": "data_exchange",
  "screen": "ID_PANTALLA_ACTUAL",
  "data": {
    "campo_del_payload": "valor_que_mando_el_flow"
  },
  "flow_token": "TOKEN_UNICO_POR_USUARIO"
}
```

Valores de `action`:
- `"INIT"`: se dispara al abrir el flow si `flow_action: "data_exchange"` en el mensaje
- `"data_exchange"`: interacción del usuario (on-select-action, Footer, etc.)
- `"ping"`: health check de Meta
- `"back"`: usuario presionó atrás y `refresh_on_back: true`

### Lo que DEBE responder tu endpoint

```json
{
  "version": "3.0",
  "screen": "ID_PANTALLA_DESTINO",
  "data": {
    "campo_declarado_en_data": "valor",
    "otro_campo_declarado": true
  }
}
```

**Regla absoluta:** cada campo en el bloque `data` de la pantalla destino DEBE estar
presente en tu respuesta. Si declaras 5 campos en el `data` block y devuelves solo 4,
el campo faltante queda en blanco o el flow falla silenciosamente.

### Devolver la misma pantalla (cálculo en tiempo real)

Para actualizar un valor sin navegar, devuelve el mismo `screen` ID:

```json
{
  "version": "3.0",
  "screen": "COTIZACION",
  "data": {
    "opciones_monto": [...],
    "pago_mensual": "$ 4,583",
    "monto_seleccionado": "m1"
  }
}
```

WhatsApp actualiza los datos en pantalla sin mover al usuario.

### Doble firma (Data API v4.0)

Dos capas de autenticación independientes:

1. **Firma de plataforma (obligatoria):** Meta firma cada request con `X-Hub-Signature-256: sha256=<hmac>`. Tu endpoint debe validarlo antes de procesar cualquier payload.

```python
import hmac, hashlib

def validar_firma(payload_bytes: bytes, header: str, app_secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        app_secret.encode(), payload_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)
```

2. **Firma de flow_token (opcional, recomendada):** presente en el payload desencriptado como `flow_token_signature`. Es un JWT firmado con tu app secret (HS256). Verifica que la solicitud viene del usuario exacto que recibió el mensaje.

```python
import jwt

def verificar_flow_token(flow_token: str, signature: str, app_secret: str) -> bool:
    try:
        decoded = jwt.decode(signature, app_secret, algorithms=["HS256"])
        return decoded.get("flow_token") == flow_token
    except jwt.InvalidTokenError:
        return False
```

### Respuesta para ping (health check)

```json
{ "data": { "status": "active" } }
```

Si no manejas el ping, tu flow queda BLOCKED.

### Protocolo de descifrado de media (PhotoPicker / DocumentPicker)

Los archivos subidos por el usuario se almacenan cifrados en el CDN de WhatsApp (hasta 20 días, solo Cloud API). Protocolo estricto en 5 pasos:

1. Descargar `cdn_file` (contiene ciphertext + últimos 10 bytes de HMAC)
2. Verificar integridad cifrada: `SHA256(cdn_file) == enc_hash`
3. Validar HMAC: `HMAC-SHA256(hmac_key, iv + ciphertext)` → primeros 10 bytes == `hmac10`
4. Descifrar: AES256-CBC con `iv`, remover padding PKCS7
5. Verificar texto plano: `SHA256(plaintext) == plaintext_hash`

Límites: 25 MiB por archivo. 10 archivos max en response message, 100 MiB combinado.
Procesa siempre en sandbox — Meta no garantiza inocuidad de archivos enviados por usuarios.

### Tipos estrictos en `__example__`

El tipo del `__example__` debe coincidir exactamente con el tipo declarado. Mismatch → `payload-schema-error` silencioso.

| Tipo declarado | `__example__` correcto | Error frecuente |
|---------------|----------------------|-----------------|
| `"string"` | `"abc"` | `0` (número) |
| `"number"` | `0` | `"0"` (string) |
| `"boolean"` | `false` | `"false"` (string) |
| `"array"` | `[{ "id": "x", "title": "X" }]` | `[]` vacío o `null` |
| `"object"` | `{}` | `null` |

Array de opciones (Dropdown / RadioButtonsGroup / CheckboxGroup):
```json
"opciones": {
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "title": { "type": "string" }
    }
  },
  "__example__": [{ "id": "op1", "title": "Opción 1" }]
}
```

### Errores HTTP que devuelve tu endpoint

| HTTP | Significado |
|------|-------------|
| 421 | No pudo desencriptar — WhatsApp re-fetchea tu llave pública |
| 427 | flow_token expirado — botón CTA se deshabilita para el usuario |
| 432 | Falló validación de firma X-Hub-Signature-256 |

---

## Por qué falla data_exchange — diagnóstico rápido

| Síntoma | Causa más probable | Solución |
|---------|-------------------|----------|
| Pantalla en blanco al navegar | Falta un campo en la respuesta del endpoint | Devolver TODOS los campos del `data` block de la pantalla destino |
| Flow no se abre | Endpoint no maneja `action: "INIT"` | Agregar handler para INIT |
| Flow pasa a BLOCKED | Endpoint no responde al `ping` | Agregar `{"data": {"status": "active"}}` al handler de ping |
| Error "payload-schema-error" | Tipo de dato incorrecto — número como string, boolean como string | Revisar tabla de tipos estrictos en sección `data_exchange` |
| Componente no se llena | Key en respuesta no coincide con `data` block | Las keys en `data` del JSON y en la respuesta deben ser idénticas |
| `on-select-action` no dispara | Componente no lo soporta | Solo: Dropdown, RadioButtonsGroup, CheckboxGroup, ChipsSelector, DatePicker, CalendarPicker, OptIn |
| Flow THROTTLED | Latencia del endpoint > 1 segundo | Target < 1s, hard limit 10s. Diseña ping para < 200ms sin tocar DB |
| Error 421 | Llave privada no coincide con pública registrada | Verificar que public.pem subida a Meta corresponde a private.pem en tu servidor |
| Error 432 | `X-Hub-Signature-256` no validada o inválida | Implementar validación HMAC con app secret antes de procesar |
| Error 131026 | Dispositivo no soporta la versión del flow | Bajar versión del flow o informar al usuario |
| `business_public_key_signature_status: MISMATCH` | Llave pública no coincide con el número de teléfono | Re-registrar llave pública en el número correcto |

---

## Componentes — referencia rápida

Lee `references/flow-json.md` para specs completas y todos los parámetros.

| Componente | Versión | Para qué | Límite clave |
|-----------|---------|---------|-----------|
| `TextHeading` | all | Título de pantalla | 80 chars |
| `TextSubheading` | all | Header de sección | 80 chars |
| `TextBody` | all | Párrafos; `markdown: true` (v5.1+) | 4096 chars |
| `TextCaption` | all | Texto pequeño; `markdown: true` (v5.1+) | 409 chars |
| `RichText` | v5.1+ | Markdown completo: headings, listas, tablas, imágenes | Standalone; v6.3+ permite Footer junto |
| `TextInput` | all | Texto, email, phone, password, passcode, number | label 20 chars, max-chars 80 |
| `TextArea` | all | Texto largo | max-length 600 chars |
| `Dropdown` | all | Select único | Max 200 opciones (100 con imágenes) |
| `RadioButtonsGroup` | all | Select único, lista corta | Max 20 opciones |
| `CheckboxGroup` | all | Multi-select | Max 20 opciones |
| `ChipsSelector` | v6.3+ | Chips horizontal multi-select | 2-20 opciones |
| `DatePicker` | all | Fecha única, devuelve `"YYYY-MM-DD"` | |
| `CalendarPicker` | v6.1+ | Fecha única o rango | |
| `Image` | all | JPEG/PNG base64 | Max 300KB c/u, 3/pantalla, 1MB total |
| `ImageCarousel` | v7.1+ | Carrusel de imágenes | 1-3 imgs, max 2/pantalla, 3/flow |
| `PhotoPicker` | v4.0+ | El usuario sube fotos | 1/pantalla; no combinar con DocumentPicker |
| `DocumentPicker` | v4.0+ | El usuario sube documentos | 1/pantalla; no combinar con PhotoPicker |
| `OptIn` | all | Checkbox de consentimiento | Max 5/pantalla; label 120 chars |
| `EmbeddedLink` | all | Link inline | Max 2/pantalla; text 25 chars |
| `NavigationList` | v6.2+ | Lista tappable | 1-20 items; max 2/pantalla; no mezclar con otros componentes |
| `Footer` | all | Botón CTA — requerido en toda pantalla terminal | label 35 chars |
| `Form` | all | Wrapper para inputs (opcional desde v4.0) | Soporta `init-values` y `error-messages` |
| `If` | v4.0+ | Renderizado condicional | Max 3 niveles anidados |
| `Switch` | v4.0+ | Renderizado multi-caso | |

Max 50 componentes por pantalla.

**v7.0+:** `label-variant: "large"` en TextInput y TextArea — label prominente sobre el campo (hasta 4 líneas).

**Imágenes en listas (v6.0+):** límite para Dropdown, CheckboxGroup y RadioButtonsGroup bajó a **100KB**. Redimensiona a 150x150px.

---

## Form component (opcional desde v4.0)

```json
{
  "type": "Form",
  "name": "mi_form",
  "init-values": {
    "monto": "${data.monto_default}"
  },
  "error-messages": {
    "monto": "${data.error_monto}"
  },
  "children": [ ...inputs... ]
}
```

Sin Form wrapper (v4.0+): usa `init-value` y `error-message` (singular) directo en cada componente.

**Regla:** si hay un Form en una pantalla, todos los inputs interactivos deben estar dentro.

---

## on-select-action con endpoint

```json
{
  "type": "Dropdown",
  "name": "plazo",
  "label": "Plazo",
  "required": true,
  "data-source": "${data.opciones_plazo}",
  "init-value": "${data.plazo_seleccionado}",
  "on-select-action": {
    "name": "data_exchange",
    "payload": {
      "monto": "${form.monto}",
      "plazo": "${form.plazo}"
    }
  }
}
```

Soportado en: `Dropdown`, `RadioButtonsGroup`, `CheckboxGroup`, `ChipsSelector`,
`DatePicker`, `CalendarPicker`, `OptIn`.

`on-unselect-action` (v6.0+): solo soporta `update_data`. Si no se define,
`on-select-action` maneja ambos eventos.

---

## Referencias globales (v4.0+)

```json
{ "type": "TextBody", "text": "${screen.PANTALLA_UNO.form.nombre}" }
```

Cuando usas referencias globales, no necesitas declarar esos campos en el `data` block
de la pantalla consumidora.

---

## Media upload components

`PhotoPicker` y `DocumentPicker` (v4.0+):
- Max 1 por pantalla; no usar ambos en la misma pantalla
- No se pueden pasar vía payload de `navigate` — usa referencias globales
- En payload de `complete` o `data_exchange`: deben ser string top-level, no anidado
- En payload de `complete`: `max-uploaded-photos`/`max-uploaded-documents` debe ser `1`
- Archivos cifrados en CDN de WhatsApp por hasta 20 días
- Ver `references/endpoints.md` para pasos de descifrado (AES256-CBC + HMAC-SHA256)

---

## Enviar el flow

### Mensaje interactivo (user-initiated)

```json
{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "PHONE_NUMBER",
  "type": "interactive",
  "interactive": {
    "type": "flow",
    "header": { "type": "text", "text": "Título" },
    "body": { "text": "Texto del mensaje" },
    "footer": { "text": "Pie opcional" },
    "action": {
      "name": "flow",
      "parameters": {
        "flow_message_version": "3",
        "flow_token": "TOKEN_UNICO",
        "flow_id": "FLOW_ID",
        "flow_cta": "Texto del botón",
        "flow_action": "navigate",
        "flow_action_payload": {
          "screen": "PRIMERA_PANTALLA",
          "data": {}
        }
      }
    }
  }
}
```

- `flow_action: "navigate"`: estático o datos ya conocidos
- `flow_action: "data_exchange"`: llama INIT al abrir el flow
- `flow_action_payload.data`: inyecta datos iniciales sin llamar al endpoint (cuando los datos son conocidos al momento de enviar)
- `flow_token`: expiración recomendada 2-3 días desde que el usuario ABRE el flow, no desde que se envía el mensaje

### Plantilla de mensaje (business-initiated)

```json
{
  "type": "BUTTONS",
  "buttons": [
    {
      "type": "FLOW",
      "text": "Abrir flow",
      "flow_id": "<FLOW_ID>",
      "flow_action": "navigate"
    }
  ]
}
```

Al enviar la plantilla, incluye el `flow_token` en los parámetros del botón.

---

## Personalización de contenido

Lee `references/personalization-patterns.md` para Flow JSON completo + código de endpoint.

| Patrón | Qué hace | Cuándo usarlo |
|--------|----------|---------------|
| **1. Datos en INIT** | Inyectar nombre, saldo, oferta al abrir | Saludo, oferta preaprobada, datos de cuenta |
| **2. Opciones en cascada** | Campo B se llena según lo elegido en A | Departamento → sucursal → fecha → horario |
| **3. Pantalla de resumen** | Endpoint construye texto con los IDs elegidos | Confirmación de cita, resumen de pedido |
| **4. Cálculo en tiempo real** | `on-select-action` devuelve misma pantalla actualizada | EMI de préstamo, cotización de seguro |
| **5. Visibilidad condicional** | `"visible": "${data.booleano}"` muestra/oculta campos | UPI vs cuenta bancaria, campos opcionales |

**Reglas rápidas:**
- Cada `${data.x}` necesita su entrada en `data` con `__example__`
- Para devolver la misma pantalla: endpoint devuelve el mismo `screen` ID
- `"visible"` y `"required"` aceptan booleanos — úsalos juntos para evitar bloquear el submit
- `"init-value"` recibe el `id` del item, no el `title`
- El endpoint debe devolver TODOS los campos del `data` block, incluso los que no cambian

---

## Restricciones absolutas

- Max **50 componentes** por pantalla
- Max **100 pantallas** por flow
- Max **10 branches** en routing model por pantalla
- Max **10 MB** para el Flow JSON completo
- Valores `null` no permitidos — omite el campo o usa `""`
- Strings vacíos `""` no permitidos en propiedades de componentes (v6.0+)
- `"SUCCESS"` es ID de pantalla reservado — nunca usarlo
- Toda pantalla terminal requiere `Footer`
- Todo `${data.campo}` necesita declaración con `__example__`
- Imágenes: JPEG/PNG, base64, max 300KB c/u, 1MB total payload
- Imágenes en listas (Dropdown, RadioButtonsGroup, CheckboxGroup): max **100KB** (v6.0+)
- `NavigationList` no puede usarse en pantalla terminal ni mezclarse con otros componentes

---

## Códigos de error frecuentes

| Código | Significado |
|--------|-------------|
| HTTP 421 | No pudo desencriptar — cliente re-fetchea llave pública |
| HTTP 427 | flow_token expirado — botón CTA deshabilitado |
| HTTP 432 | Falló validación de firma X-Hub-Signature-256 |
| `invalid-screen-transition` | Pantalla siguiente no está en el routing model |
| `payload-schema-error` | Datos de pantalla no coinciden con el schema declarado |
| `timeout_error` | Endpoint tardó más de 10 segundos |
| `DUPLICATE_FORM_COMPONENT_NAMES` | Dos inputs comparten el mismo `name` en la misma pantalla |
| `MISSING_FOOTER_ON_TERMINAL_SCREEN` | Pantalla terminal sin Footer |
| `SCREEN_ID_IS_RESERVED_KEYWORD` | Usaste `"SUCCESS"` como ID de pantalla |
| Error 131026 | Dispositivo no soporta la versión del flow especificada |

---

## Ciclo de vida del flow

| Estado | Descripción |
|--------|-------------|
| `DRAFT` | Solo se puede enviar a números de prueba con `"mode": "draft"` |
| `PUBLISHED` | En producción. Desde v6.0 se puede editar el JSON sin cambiar el ID |
| `THROTTLED` | Limitado a 10 mensajes/hora por baja salud del endpoint |
| `BLOCKED` | Suspensión total — usuarios no pueden abrir ni recibir el flow |
| `DEPRECATED` | Versión de flow JSON congelada; migrar a v7.3 |

Recuperación automática: cuando el endpoint mejora sus métricas, el flow regresa
de BLOCKED a THROTTLED y luego a PUBLISHED sin intervención manual.

---

## Gestión vía Graph API

### Crear flow
```bash
curl -X POST "https://graph.facebook.com/v24.0/{WABA_ID}/flows" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nombre_flow",
    "categories": ["APPOINTMENT_BOOKING"],
    "endpoint_uri": "https://tu-servidor.com/whatsapp-flow"
  }'
```

Categorías válidas: `SIGN_UP`, `SIGN_IN`, `APPOINTMENT_BOOKING`, `LEAD_GENERATION`,
`CONTACT_US`, `CUSTOMER_SUPPORT`, `SURVEY`, `OTHER`.

### Subir Flow JSON
```bash
curl -X POST "https://graph.facebook.com/v24.0/{FLOW_ID}/assets" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -F "name=flow.json" \
  -F "asset_type=FLOW_JSON" \
  -F "file=@./flow.json;type=application/json"
```

### Registrar llave pública RSA
```bash
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

curl -X POST "https://graph.facebook.com/v24.0/{PHONE_NUMBER_ID}/whatsapp_business_encryption" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  --data-urlencode "business_public_key=$(cat public.pem)"
```

Cada número de teléfono en tu WABA necesita su propia llave pública registrada.

### Publicar
```bash
curl -X POST "https://graph.facebook.com/v24.0/{FLOW_ID}" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -d "status=PUBLISHED"
```

### Clonar flow (control de versiones)
```bash
curl -X POST "https://graph.facebook.com/v24.0/{WABA_ID}/flows" \
  -H "Authorization: Bearer {ACCESS_TOKEN}" \
  -d '{
    "name": "nombre_flow_v2",
    "clone_flow_id": "{FLOW_ID_ORIGINAL}"
  }'
```

Los flows publicados no se pueden eliminar. Clona para iterar versiones manteniendo
el flow anterior operativo.

---

## Checklist antes de publicar

- [ ] Flow JSON v7.3 y `data_api_version: "4.0"` (string, no número)
- [ ] Negocio verificado en WABA con calidad de mensaje "Alta"
- [ ] Flow vinculado a una Meta App activa con permisos `whatsapp_business_messaging`
- [ ] Llave RSA 2048 bits generada con `-des3`, pública cargada por número de teléfono
- [ ] `business_public_key_signature_status: VALID` confirmado
- [ ] Endpoint HTTPS con certificado válido (no self-signed en producción)
- [ ] Health Check exitoso en Flow Builder
- [ ] Endpoint responde `ping` en < 200ms con `{"data": {"status": "active"}}` (sin tocar DB)
- [ ] Validación `X-Hub-Signature-256` implementada en el endpoint
- [ ] Endpoint maneja `INIT` si `flow_action: "data_exchange"` en el mensaje de disparo
- [ ] Endpoint maneja `back` si alguna pantalla tiene `refresh_on_back: true`
- [ ] Toda pantalla terminal tiene Footer con acción `complete`
- [ ] Todo `${data.campo}` declarado en `data` con `__example__` del tipo correcto
- [ ] Respuesta del endpoint incluye TODOS los campos del `data` block de la pantalla destino
- [ ] No hay valores `null` ni strings vacíos en el JSON (v6.0+)
- [ ] No se usa `"SUCCESS"` como ID de pantalla
- [ ] No hay `name` duplicados dentro de la misma pantalla
- [ ] Imágenes en listas: max 100KB (v6.0+), redimensionadas a 150x150px
- [ ] Campos sensibles (OTP, password, docs): declarados en `sensitive[]` a nivel de pantalla
- [ ] `refresh_on_back: true` solo en pantallas que necesiten datos frescos del servidor
- [ ] Suscripción a webhooks activa en la WABA para alertas de salud del flow

---

## Cómo abordar una solicitud de Flow JSON

1. Clarifica pantallas y flujo de datos si no es obvio
2. Decide: estático (solo navigate) o dinámico (data_exchange necesario)
3. Versión: siempre `7.3` salvo indicación contraria
4. Para cada pantalla: lista campos y de dónde vienen (endpoint, navigate payload, referencia global)
5. Escribe el JSON — pantallas en orden, bloques `data` primero, luego layout
6. Si el flow es dinámico, escribe también el esqueleto del endpoint con todos los handlers (INIT, ping, data_exchange por pantalla)
7. Verifica:
   - Todo `${data.x}` declarado con `__example__`?
   - Toda pantalla terminal tiene Footer + `complete`?
   - `routing_model` cubre todas las pantallas?
   - Sin `name` duplicados en la misma pantalla?
   - Sin valores `null`?
   - Respuesta del endpoint incluye todos los campos del `data` block destino?

Para specs completas de componentes: `references/flow-json.md`
Para protocolo de encriptación/desencriptación: `references/endpoints.md`
Para ejemplo completo funcionando: `references/example-loan-flow.json`
Para patrones de personalización con Flow JSON + endpoint: `references/personalization-patterns.md`

---

## Best practices

**Estructura y UX**
- Target: < 5 minutos para completar el flow
- Max 1 tarea por pantalla
- Pocos componentes por pantalla — muchos componentes cargan más lento
- Títulos en sentence case. CTAs descriptivos: "Confirmar cita", no "Siguiente"
- Pantalla final siempre con resumen de lo que el usuario envió

**Endpoint**
- Si los datos del primer screen son conocidos al enviar: usa `flow_action_payload.data` en lugar de INIT
- Usa `navigate` cuando el siguiente screen y sus datos ya son conocidos
- Omite `refresh_on_back` si no necesitas comportamiento custom al presionar atrás
- Diseña el handler de `ping` para responder en < 200ms sin consultar DB — es el primer indicador de salud
- Latencia target para `data_exchange`: < 1s. Hard limit: 10s. THROTTLED activa a > 1s sostenido
- Optimiza imágenes: redimensiona a 150x150px, comprime, convierte a base64
- Usa referencias globales para evitar pasar datos entre pantallas manualmente
- Implementa siempre el handler de `ping` — sin él el flow queda BLOCKED automáticamente
- Monitorea `business_public_key_signature_status` — si es `MISMATCH`, el intercambio de datos se detiene completamente

**Payload y datos sensibles**
- En `complete` payload: solo datos ingresados por el usuario. No incluir imágenes base64
- Campos sensibles: declara en `sensitive` a nivel de pantalla
- `flow_token`: guárdalo para identificar al usuario/sesión en tu backend

**Manejo de errores**
- Valida `X-Hub-Signature-256` en tu endpoint para confirmar que el request viene de Meta
- Si un screen se vuelve inválido, regresa al screen anterior en lugar de terminar el flow
- Usa `error-message` en componentes o `error-messages` en Form para comunicar errores

---

## Testing y debugging

**3 opciones:**

1. **Interactive Preview** (recomendado durante desarrollo)
   - Flows Manager → click en el flow → habilitar "Interactive mode" en Preview
   - Activa las mismas acciones que un dispositivo real, incluyendo llamadas encriptadas al endpoint
   - Tab "Actions": request/response sin encriptar

2. **Enviar flow en modo draft al dispositivo**
   - Flows Manager → flow en Draft → menu tres puntos → Send
   - Disponible vía API con `"mode": "draft"`

3. **Health Check del endpoint**
   - Flow Builder → menu tres puntos → Setup → Endpoint → Health check → Run Check
   - Si falla recurrentemente: el flow entra en estado BLOCKED
