El chooser (solicitud/[id]) ahora muestra un stepper de 3 pasos (Tus datos ✓, Tu reforma actual, Render + presupuesto pendiente) y, debajo de las tarjetas de canal, una sección "Elijas lo que elijas, esto es lo que pasa después" con los 3 pasos del flujo (nos cuentas tu reforma, render + presupuesto en minutos, visita gratuita para el presupuesto final).
Las tarjetas de canal pasan a grid de 3 columnas en desktop, con iconos SV
La galería de trabajos (GaleriaTrabajos) en la landing personalizada del cliente
pasa a formato apaisado (3/2) y, al pulsar una foto, se amplía en un lightbox con
navegación ‹ ›, Esc y clic fuera para cerrar. Se quita el botón "Entrar" del
header de esa landing (TenantBrand sin showLogin): el cliente final no entra al
panel.
Revierte el cambio anterior en public/b2b.html (era la landing equivocada).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El docker-entrypoint corre db:seed en cada arranque. El guard comprobaba si
existía un tenant CONCRETO ("reformas-ejemplo"); si ese no estaba pero había
otros (una empresa creada por el reformista), el seed ejecutaba TRUNCATE de
todas las tablas y resembraba el demo → borraba los datos reales en cada deploy.
Ahora el guard salta si existe CUALQUIER tenant, así que con datos reales el seed
nunca toca la DB. SEED_FORCE=1 sigue forzando el reseed (borra todo) a propósito.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Nueva sección "Ejemplos reales": 3 renders (cocina, baño, salón-comedor) en
formato apaisado (16/10), clicables.
- Lightbox reutilizable (data-zoom + data-gallery): amplía la imagen con
navegación ‹ ›, Esc y clic fuera para cerrar; agrupa por galería.
- El antes/después de "Cómo funciona" pasa a apaisado (4/3) y también abre el
lightbox (grupo cocina).
- Quita el botón "Entrar" del header (queda solo "Empezar gratis").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tour por pestañas que explica los puntos clave: en Leads recorre la navegación
(las pestañas secundarias de pasada) + filtros y tabla; en la ficha del lead el
presupuesto/baremo, estado, render y desglose; en Precios el baremo, la mano de
obra y el catálogo. Auto-arranca la primera vez por pestaña (flag en localStorage)
y deja un botón flotante "❓ Tour" para repetir. Pasos sin elemento visible se
descartan (degrada en móvil).
- Dependencia: driver.js (librería estándar de tours, ~5kb, sin más deps;
evita reinventar overlay/posicionamiento/foco/accesibilidad).
- src/lib/onboarding/panel-tour.ts: pasos por ruta. PanelTour.tsx: cliente que
lanza driver.js. data-tour en nav, leads, ficha y precios.
- Copy en COPY-GUIDE.md (sección "Onboarding del panel").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parte C del plan: el baremo mínimo de rentabilidad es ahora un valor configurable
del reformista, solo informativo. Los agentes NO lo usan para decidir nada.
- schema: pricing_config.baremo_minimo (céntimos, nullable) + migración 0012.
- pricing-queries / budget types: exponen baremoMinimo.
- panel/precios: sección "Baremo de rentabilidad" + action actualizarBaremo
(vacío = sin baremo).
- panel/[id]: el presupuesto estimado se muestra en rojo con aviso "Por debajo
de tu baremo (X €)" cuando no alcanza el baremo del tenant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Añade al formulario del sandbox dos inputs opcionales (estilo, gustos) y los
manda en el body de /sandbox/render, para probar desde la web cómo las
preferencias del cliente condicionan el render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hasta ahora el render solo se condicionaba con tipo/m²/calidad + notas de texto
libre por zona; lo que el cliente decía hablando con Luisa o en la llamada
(estilo, colores, materiales) se guardaba en estilo/tasteText pero NO viajaba al
generador de imagen, así que el render no lo representaba.
- b2c (perfil.ts): el payload de PERFIL_WEBHOOK_URL incluye ahora
preferencias:{estilo, gustos} (gustos = tasteText). Claves vacías se omiten.
- worker (webhook.dto): nuevo PreferenciasDto opcional.
- worker (prompt-builder): construirUserContent (función pura) inyecta el estilo
y los gustos del cliente como bloque dedicado y omite el "modern" por defecto
cuando hay preferencias; el system prompt prioriza colores/materiales del
cliente sobre un estilo genérico.
- worker (pipeline): enhebra preferencias hasta generarPrompt.
- worker (sandbox): acepta estilo/gustos para poder probarlos.
- docs/arquitectura-integracion: documenta el campo preferencias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El bot rechazaba leads con presupuesto bajo y sonaba frío/repetitivo. Ahora
Luisa nunca rechaza (el cierre siempre es positivo; la rentabilidad la valora
el reformista en el panel, no el agente) y tiene el tono del agente de voz:
simpática, variada y dispuesta a ayudar con referencias.
- leads.service: getSiguienteEstado tras presupuesto siempre fin_viable;
evaluarViabilidad ya no filtra (informa viable=true).
- claude.service: relaja las reglas de la pasada de corrección (permite calidez,
conectores, emoji suave, 2-3 líneas, variación) y quita el guard que tomaba
"en qué puedo ayudarte" como frase de IA.
- prompts: reescribe el guion de Luisa (mensajes como ejemplos variables, ayuda
con referencias de tamaño/materiales, sin rechazo) y elimina el bloque
<DATOS_EXTRAIDOS>, que era código muerto contradictorio con el generador.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El PDF llegaba con render+fotos pero sin importe porque nadie ejecutaba el motor.
Nuevo helper calcularPresupuestoLead (reutiliza computeBudget + catálogo del
orquestador). Se llama tras el post-análisis (WhatsApp/llamada) y, como red de
seguridad, al inicio de finalizarYEntregar antes de construir el PDF.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bot: al cerrar la cualificación, Luisa pide una foto y entra en modo recogida;
al llegar la foto la sube como "antes" con perfilCompleto:true → dispara render +
presupuesto + entrega del PDF. Nuevo webhook /whatsapp-fotos para que, tras una
llamada, Luisa escriba al lead, referencie lo hablado y le pida las fotos
(reutiliza el mismo modo).
App: el webhook de Retell, tras el análisis de la llamada, llama a pedirFotosWhatsapp
(WHATSAPP_FOTOS_WEBHOOK_URL) con el contexto de la reforma.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generaliza analizarTranscripcion(leadId, transcript, origen) como núcleo agnóstico
del canal. WhatsApp arma la transcripción desde conversacion_whatsapp; el webhook
de Retell, tras guardar leads.transcripcion, llama al mismo agente con la
transcripción de la llamada → ambos canales extraen espacio/m2/calidad/urgencia/
presupuesto/viable con un único cerebro LLM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cuando la conversación llega al cierre (estado presupuesto/fin_*, o Luisa anuncia
que prepara/envía el presupuesto), el bot llama una vez a POST /api/leads/:id/analizar
para que la app capture todos los datos clave de la conversación de una pasada.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/leads/:id/analizar lee toda la conversacion_whatsapp del lead, extrae
con un LLM (OpenRouter) los datos clave (tipoReforma, m2, calidad, urgencia,
presupuesto, viable + crudos) y los persiste en el lead de una pasada. Robusto
frente a la extracción turno-a-turno frágil del bot. El bot lo llamará al cerrar
la cualificación. Helper lib/ai/openrouter.ts + env OPENROUTER_API_KEY.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Con false, al reanudar la sesión guardada el dispositivo quedaba "no disponible"
y WhatsApp no le entregaba los mensajes entrantes (socket open pero inbound=0).
Con un escaneo fresco sí recibía (por el sync de emparejamiento). Marcando online
al conectar, WhatsApp entrega los mensajes también tras reconectar/redeploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmado vía /debug: WhatsApp entrega los mensajes desde una dirección @lid
(p.ej. 239225534443615@lid), no desde el número. El bot tomaba el id del lid
como teléfono -> no casaba con ningún lead -> ignoraba el mensaje. Ahora
resolverTelefono() resuelve el @lid a número vía msg.key.remoteJidAlt o el mapa
LID->PN de Baileys antes de buscar el lead. Debug captura también remoteJidAlt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BAILEYS_AUTH_DIR permite arrancar una sesión limpia (QR fresco) sin perder la
persistencia (subcarpeta del volumen) — para re-vincular cuando WhatsApp deja
muerto el dispositivo tras varios reinicios. Y messages.upsert ahora registra
en /debug todos los eventos (incluido type != notify) antes de filtrar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /debug (Basic auth, QR_TOKEN) devuelve el estado de la conexión de WhatsApp
y un anillo de los últimos mensajes entrantes (remoteJid real, fromMe, tipo) y
el resultado del matching lead↔teléfono. Para diagnosticar por qué el bot no
responde (conexión zombi vs formato de jid @lid vs no llega el mensaje).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El bot no respondía a las réplicas del cliente: la sesión lead↔teléfono vivía
solo en memoria y no casaba (se pierde al reiniciar el contenedor). Ahora si no
está en memoria, getOrCreateContext busca el lead en la BD por teléfono vía un
EP nuevo GET /api/leads/by-phone (match por últimos 9 dígitos) y re-registra la
sesión. Aparte, los ids de modelo de Claude del bot estaban mal (-4-5 vs 4.5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El funnel promete "te escribimos por WhatsApp" pero el bot solo registraba la
sesión y esperaba a que el cliente escribiera primero → no llegaba nada. Ahora
WhatsappService escucha un startEmitter y manda el mensaje de apertura de Luisa
al teléfono (verifica el número con onWhatsApp), persiste estadoWa/botStep y el
intento. Además normaliza la clave de teléfono a solo-dígitos en leadSessions
(antes "+34..." no casaba con los dígitos del jid entrante → ignoraba al cliente).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review de seguridad: autenticar por query string filtra el secreto (logs,
historial, Referer) y usaba la misma FUNNEL_API_KEY que autoriza la API.
Ahora /qr usa HTTP Basic (credencial en cabecera), un QR_TOKEN dedicado
distinto de FUNNEL_API_KEY, comparación en tiempo constante (timingSafeEqual
sobre hashes) y Referrer-Policy: no-referrer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El QR de Baileys solo salía como ASCII en los logs (ilegible en Dokploy). Ahora
el WhatsappService empuja el QR al WebhookListener, que sirve GET /qr?key=
<FUNNEL_API_KEY> con el código como imagen (lib qrcode), autorrefresco y estado
"ya conectado". Añade dependencia qrcode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sandbox web (/sandbox) servido por el worker, reusando los mismos servicios del
pipeline, para iterar prompts/modelos con una imagen y ver variaciones+score, y
guardar la config ganadora (aplica al worker al instante, persistida en volumen):
- SettingsService: store de config (system prompts + modelos) con defaults de
prompts/*.txt + env y override persistido en /app/data (sobrevive a redeploys).
- /sandbox (HTML), GET /sandbox/config, POST /sandbox/render, POST /sandbox/save
(auth Bearer FUNNEL_API_KEY). DOM seguro, sin innerHTML de contenido externo.
- prompt-builder/supervisor/image-generator aceptan overrides y leen de Settings.
Fixes del pipeline de generación:
- image-generator: pide modalities ['image','text'] y extrae la imagen de
message.images[] (forma real de OpenRouter), no solo de content.
- main.ts: sube el límite de body a 30mb (las fotos en data URI rompían el 100kb).
Deja de versionar artefactos de build (dist/ + *.tsbuildinfo); .gitignore en
image-worker y Whatsapp-bot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Dockerfiles multistage (node:20-alpine) + .dockerignore para mvp/Whatsapp-bot
(expone 3000+3001, persiste prompts) y mvp/image-worker (expone 3001).
- Añade los 2 GET que el bot necesita y faltaban en la API:
GET /api/leads/:id (estado del lead) y GET /api/leads/:id/conversacion (historial).
- Limpia el package.json/lock de la raíz (baileys colado por error; no es workspace).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El bot de Luisa puebla la BD vía los 4 EPs HTTP (no SQL directo): conversacion,
perfil, calificacion, intento. Actualiza el handoff de Simón en consecuencia
(qué EP usa para cada cosa, enums/tipos a alinear, ya no necesita acceso a BD).
Añade api-docs/smoke-bot-eps.mjs: crea un lead de prueba, ejerce los 4 EPs por
HTTP, verifica en BD y limpia. Verificado end-to-end en produccion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El bot (Luisa) es externo y no toca Postgres directamente. Cuatro endpoints
autenticados con Bearer FUNNEL_API_KEY, validados con zod:
- POST /api/leads/:id/conversacion → turno de chat (+ estado_wa/bot_step)
- POST /api/leads/:id/perfil → update parcial del lead (extracción)
- POST /api/leads/:id/calificacion → upsert de lead_calificacion
- POST /api/leads/:id/intento → registro en intentos_contacto
Helpers compartidos lib/api/funnel-auth.ts (autorizado + jsonResponse) y
lib/api/bot-request.ts (validarBotRequest: auth + JSON + zod + lead existe).
La ruta de ingesta se refactoriza para reutilizar funnel-auth (DRY).
Schemas puros en lib/funnel/bot-schemas.ts con tests, y doc en api-docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arregla 2 problemas del flujo de subir fotos desde el email:
- El enlace iba a /formulario (form completo) y al enviarlo re-ejecutaba
procesarLead, que VOLVÍA a llamar. Ahora el email apunta a /solicitud/[id]/fotos,
una página ligera (SubirFotos): solo sube fotos (+ nota opcional) al lead de la
URL, re-señala perfilCompleto y NO llama.
- Guarda en procesarLead: si el lead ya tiene llamada_completada, no se vuelve a
llamar (ni se pisa la transcripción real del webhook).
Copy de la página en COPY-GUIDE §3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El gate del allowlist miraba allow.length>0 (tras parsear), así que un typo en
RETELL_ALLOWED_NUMBERS que no dejara ningún número válido abría las llamadas a
todos (fail-open). Ahora mira la presencia de la variable: si está puesta, se
enforce; un typo restringe a nadie (fail-closed), acorde a la intención.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guarda en iniciarLlamadaSaliente: si RETELL_ALLOWED_NUMBERS tiene valor (CSV de
E.164), solo se lanza la llamada a esos números; el resto se omite (devuelve
null, funnel en simulado). Vacío = se llama a todos. Protege tanto el form
(procesarLead) como el canal llamada (pedirLlamada). En prod = +34651194617.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Si la llamada no lleva metadata.lead_id (llamadas manuales/antiguas), el webhook
busca el lead más reciente con ese teléfono (últimos 9 dígitos) y le asocia la
transcripción + grabación. Responde JSON informativo (matched/leadId) para poder
re-procesar llamadas a mano.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- retell.ts: la llamada saliente manda metadata.lead_id; helpers obtenerLlamada
(GET /v2/get-call, dato autoritativo) y descargarGrabacion (guarda el audio
en nuestro sistema como data URI).
- /api/retell/webhook: en call_analyzed/call_ended relee la llamada por call_id,
guarda la transcripción real en lead.transcripcion, descarga la grabación a
lead.audio_url y deja el análisis + duración en un evento de pipeline. Seguro
por re-fetch (no se fía del body del webhook).
- orchestrator/pedirLlamada: pasan leadId; procesarLead ya no guarda transcript
simulado cuando la llamada es real (lo rellena el webhook).
- La ficha del panel ya mostraba transcripción + audio: ahora se pueblan solos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Prompt del agente de voz (Retell, en caliente + docs/retell-setup.md):
recuerda al cliente que envíe fotos del espacio por WhatsApp o por el enlace
del email, para que el render sea de su reforma.
- pedirLlamada: envía automáticamente el email con el enlace al formulario al
solicitar la llamada (antes solo con un botón manual), para que la frase del
agente "te enviamos un email" sea cierta. Best-effort.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Migración 0011: leads.bot_step (TEXT) = paso actual de la conversación del
bot (Luisa), para verlo en el panel y poder retomar chats cortados. TEXT (no
enum) para que el bot evolucione su vocabulario sin migración.
- docs/handoff-whatsapp-simon.md: spec de integración del bot (DB única, lead
desde el form, reparto DB-directa vs EP, tablas que escribe, alineación de
enums/tipos a los nuestros, bot_step, webhooks y conectividad).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Debajo del hero, nueva sección #demo: título + placeholder de vídeo 16:9
(sustituible por iframe/video cuando lo haya) + CTA "Empezar ahora". El botón
"Ver una demo en vivo" del hero apunta aquí (se movió el id #demo del mock del
hero a esta sección). Copy añadido a COPY-GUIDE §2.
Aplicado en public/b2b.html (servido en / y /b2b) y en mvp/b2b/index.html.
Nota: ambos ficheros ya tenían el hero divergido (public con copy más nuevo);
no toco ese copy aquí.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integra el esquema "reformix-full" del equipo de forma ADITIVA, sin tocar los
enums ni columnas existentes de la app (una sola DB, Drizzle es el dueño):
- Enums nuevos: estado_wa, canal_contacto, canal_origen, resultado_contacto,
rol_mensaje, job_tipo, job_estado, nivel_calificacion, visita_estado.
- Tablas nuevas: conversacion_whatsapp, intentos_contacto, lead_calificacion
(score 0-100 + nivel A/B/C/D), visitas, worker_jobs (cola async de los
workers de fotos/render/presupuesto). Referencian nuestros leads/users/tenants.
- Columnas nuevas en leads (nullable, las rellena el bot/Luisa): estado_wa,
canal_origen, espacio, rango_m2, estilo, presupuesto_declarado, viable,
fotos_solicitadas_at.
- Migración 0010 + db-schema/schema.sql regenerado.
El bot/n8n escribe estas tablas en la DB única y usa nuestros leads (creados
solo desde el form web). Pendiente: alinear valores de lead_estado/pipeline_stage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prepararLogo: el logo (data URI base64, URL http o ruta /public con APP_URL)
se adjunta como imagen inline con Content-ID y se referencia con cid:, que
es la forma fiable de mostrarlo en Gmail/Apple Mail/Outlook (el data URI
directo lo bloquea Gmail). Fallback al nombre de la empresa si no hay logo.
- El acento (barra, botón) ya usa el color de marca del tenant (resolveTheme).
Probado: envío real con el logo de Reformas Ejemplo embebido y tema pizarra.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reescribe los emails del funnel (entrega del presupuesto + enlace al
formulario) con plantilla HTML mobile-first de una columna, dark mode, botón
"bulletproof" en tabla, tipografías de sistema y versión en texto plano.
- mailer.ts: construirEmailHtml/construirEmailText compartidos; acento en el
color de marca del reformista (resolveTheme) + logo si es URL absoluta; copy
con jerarquía y CTA orientado a resultado.
- finalizar.ts: pasa la marca del tenant y un CTA de contacto (wa.me del
reformista o mailto) al email de entrega.
- actions.ts: pasa la marca al email con el enlace al formulario.
- COPY-GUIDE §6.b: asuntos (+alternativas), preheader, headline y cuerpo
mejorados.
Probado: render visual (light) de ambos emails y envío real por SMTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Acerca el cálculo a tarifas de mercado sin rehacer el modelo lineal €/m²:
- Impermeabilización como partida propia en zonas húmedas (cocina/baño/integral)
- Extras fijos que no escalan con m²: boletín (siempre), tuberías (piso anterior
a 2000) y cambio de distribución (mover inodoro/ducha/bañera)
- Intensidad por tipo en fontanería/electricidad (baseline cocina) para que un
integral no escale como un baño
- Factor de zona por provincia en tramos (Madrid/BCN 1.40, islas 1.30, capitales
1.20, rural 0.85, resto 1.00)
- 2 preguntas nuevas en el formulario del cliente para disparar los extras
- Panel de precios: campo de impermeabilización + sección de extras fijos
- Seed recalibrado (mano de obra, extras, catálogo suelo/pared)
- Migración 0009 (leads.anterior_a_2000, leads.cambio_distribucion, pricing_config.extras)
- Tests del motor ampliados (impermeabilización, extras, intensidad por tipo)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- agruparPorZona (lib/funnel/fotos.ts): helper puro que agrupa fotos
(antes/después) y notas por zona, con fallback al tipo del lead. 5 tests.
- getLead trae también lead_notas.
- LeadFotosGaleria: galería por zona (Antes/Después + notas) que sustituye el
grid plano de la ficha del panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Llamada (/llamada): "Llamar ahora" dispara la llamada saliente de Retell;
"Programar" registra fecha/hora. Tras pedir, ofrece enviar por email el
enlace al formulario para subir las imágenes.
- WhatsApp (/whatsapp): arranca la conversación vía webhook al flujo externo
(con el teléfono del lead) y pide confirmación; al confirmar, agradece y
sigue por WhatsApp.
- actions: pedirLlamada, enviarEnlaceFormularioEmail, iniciarWhatsapp,
confirmarWhatsapp.
Verificado en navegador: las 3 pantallas y sus transiciones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Paso intermedio /solicitud/[id]: el cliente elige llamada, WhatsApp o
formulario (crearLead ahora redirige aquí, no a /fotos).
- /formulario: FormularioZonas permite añadir varias zonas, cada una con tipo,
m², acabado, notas y fotos; /fotos queda como redirect.
- guardarDetallesYFotos: guarda fotos (antes, por zona) y notas (por zona),
agrega los campos del lead (m² suma, tipo único o 'integral', calidad más
alta, tasteText concatenado) para el presupuesto orientativo inmediato, y
señala perfilCompleto al flujo externo.
- Elimina FotosUploader (sustituido por FormularioZonas).
Verificado en navegador: 2 zonas → presupuesto al instante + notas por zona +
evento de perfil en DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
api-docs/README.md: método/URL, auth Bearer, esquema del cuerpo (items
foto/texto, zona, momento, flags), respuesta y errores (401/404/422), ejemplos
curl de los 5 casos, y los payloads de los 3 webhooks salientes (perfil,
entrega WhatsApp, arranque WhatsApp) con cómo el flujo externo devuelve las
"después" por el mismo EP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>