Documentación API — Growth54
https://api.growth54.com
Growth54 es una plataforma de control empresarial multiempresa orientada a automatización, contenido y crecimiento asistido por IA. Su API REST permite que el panel web, los agentes de inteligencia artificial y los workflows de n8n lean y escriban datos de cada empresa registrada.
La API tiene cuatro capas de acceso según quién llama: usuarios del panel (Sanctum), administradores master, automatizaciones de n8n (solo lectura) y agentes de IA (escritura de resultados). Esta separación permite que cada integración tenga exactamente los permisos que necesita.
Esta guía cubre todos los endpoints disponibles, explica el propósito de cada módulo y documenta el brief técnico para construir los agentes n8n del módulo RRSS.
Convenciones
Reglas que aplican a todas las llamadas, independientemente del módulo o tipo de acceso. Leer esto antes de integrar cualquier endpoint.
N8N_API_TOKEN o AGENT_API_TOKEN están vacíos en el .env, esas rutas aceptan llamadas sin token. Esto facilita las pruebas iniciales. Para pasar a producción: definir ambas variables en el .env del backend y reiniciar los contenedores Docker.
Headers obligatorios en toda llamada
Accept: application/json
Content-Type: application/json
Códigos de error comunes
- 401 — Token no enviado o inválido.
- 403 — Sin permiso para esa acción.
- 422 — Datos incorrectos o empresa activa no asignada.
- 404 — Recurso no existe o no pertenece a tu empresa.
Tipos de acceso
La API distingue cuatro tipos de caller, cada uno con su propio prefijo de ruta y mecanismo de autenticación. La separación es intencional: un agente de IA solo puede escribir datos de resultados, no acceder a la configuración del usuario; un workflow de n8n solo puede leer, no modificar.
| Tipo | Prefijo | Quién lo usa |
|---|---|---|
| Usuario del panel | /api/auth/* | Frontend — token Sanctum |
| Admin master | /api/admin/* | Administrador de plataforma |
| n8n / automatizaciones | /api/n8n/* | Workflows — solo lectura |
| Agentes de IA | /api/agent/* | Agentes — escritura de datos |
| Público | /api/public/* | Sin token, con rate limit |
422. Se cambia con POST /api/auth/switch-company pasando el company_id deseado. La empresa activa es de cada sesión, no de la cuenta: dos tokens del mismo usuario pueden estar en empresas distintas al mismo tiempo New · 6-ago.
Registro público
sin token · rate 5/minEstos endpoints no requieren autenticación. Están diseñados para recibir leads desde formularios externos — una landing page en WordPress, un anuncio con formulario, etc. El rate limiting de 5 solicitudes por minuto evita que se use como vector de spam. Al registrar un cliente, el sistema crea el usuario en estado pendiente y lo asigna al flujo de onboarding.
Registra un cliente desde formulario externo (WordPress, landing page, etc.).
curl -s -X POST "https://api.growth54.com/api/public/register-client" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"name":"Juan García","email":"juan@empresa.com"}' | jq
Verifica que la app, base de datos y caché respondan.
curl -s "https://api.growth54.com/api/health" | jq
Login de usuario
SanctumEl flujo estándar para el panel web. El login devuelve un token Bearer de Laravel Sanctum que se incluye en todas las llamadas posteriores. El token no expira automáticamente — se invalida llamando a logout. Después del login, el primer paso es llamar a switch-company para activar la empresa con la que se quiere trabajar.
curl -s -X POST "https://api.growth54.com/api/auth/login" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"email":"user@demo.com","password":"secret"}' | jq
Datos del usuario autenticado.
Cambia la empresa activa. Necesario antes de usar módulos.
curl -s -X POST "https://api.growth54.com/api/auth/switch-company" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"company_id": 1}' | jq
switch-company, todas las demás pasaban a ver esa empresa sin haber tocado nada. Ahora vive en el token: cada sesión (pestaña, dispositivo, script) mantiene su propia empresa activa y switch-company solo afecta al token con el que se llamó. La columna de users queda como recuerdo de la última empresa usada y es con la que arranca cada login nuevo. Impacto para integraciones: un script que hacía login una vez y esperaba que otro proceso le cambiara la empresa ya no funciona — cada token debe llamar a su propio switch-company. Los tokens creados antes del cambio se sellan solos en la primera llamada a /api/auth/me.Invalida el token actual.
Admin master
is_master = truecurl -s -X POST "https://api.growth54.com/api/admin/login" \
-H "Content-Type: application/json" \
-d '{"email":"admin@demo.com","password":"secret"}' | jq
GET lista · POST crear · GET/{id} · PUT actualizar · DELETE
Al crear con owner_user_id asigna rol owner automáticamente.
New · 19-ago Al actualizar, features se fusiona, no se reemplaza: lo que mandes se actualiza y las claves que no mandes se conservan. Antes se reemplazaba el bloque entero, así que un guardado desde una pantalla que solo conoce parte de las claves borraba el resto en silencio.
Las características conviven en dos formas y conviene conocerlas: como módulos dentro de features.modules[] (que es como los gestiona la pantalla de Planes) y como casillas sueltas de primer nivel, que es lo que escribe el seeder. Quien comprueba un permiso debe aceptar las dos: mirar solo una deja fuera a la mitad de los planes.
Onboarding
auth:sanctumEl proceso que sigue un usuario recién registrado. Tiene dos caminos: crear una empresa nueva (y quedar como owner) o unirse a una empresa existente usando un código de invitación (y quedar como collaborator). Los códigos de invitación los generan los owners o admins de la empresa, y se pueden limitar en número de usos y fecha de expiración.
Crea empresa y asigna al usuario como owner.
curl -s -X POST "https://api.growth54.com/api/onboarding/companies" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"name":"Mi Empresa","industry":"Legal","website":"https://mi.com"}' | jq
Unirse con código de invitación. Rol: collaborator.
-d '{"code":"ABC123"}'
Genera código de invitación. Solo owner o admin.
-d '{"max_uses": 10, "expires_in_days": 30}'
Keywords
auth:sanctumLas keywords son el punto de partida del motor de contenido SEO. Cada empresa mantiene un banco de palabras clave clasificadas por dificultad, intención de búsqueda y fuente de origen. Los agentes de IA usan este banco para generar artículos relevantes y posicionar el sitio de la empresa. Las keywords pueden agregarse manualmente desde el panel o en bulk desde un workflow de n8n que haya hecho keyword research.
/api/agent/keywords/bulk → el usuario las revisa en el panel → aprueba las que le interesan → el agente generador de artículos las convierte en contenido.Lista keywords de la empresa activa.
Totales por dificultad, intención, etc.
Crea keyword manual. Valores: dificultad_nivel: baja/media/alta · intencion: informacional/transaccional/comercial/local
-d '{"keyword":"abogado laboral","dificultad_nivel":"media","intencion":"comercial","fuente":"Google"}'
Dispara el agente de keyword research para la empresa activa.
Dispara generación de artículo para una keyword específica.
Artículos
auth:sanctumLos artículos son el contenido SEO generado por los agentes a partir de las keywords. Cada artículo parte de una keyword, el agente genera el HTML completo y lo guarda como borrador. Desde el panel, el equipo lo revisa, edita si necesita y lo marca como revisado. Luego puede publicarse directamente en WordPress mediante la integración CMS. El listado no incluye el HTML para no sobrecargar la respuesta — se obtiene con el endpoint de detalle.
Lista (sin HTML). estado: borrador/revisado/publicado
Detalle completo con HTML.
Marca como publicado y asigna fecha.
Auditorías
auth:sanctumLas auditorías son reportes de análisis completos generados por agentes. A diferencia de la auditoría inicial (que es única por empresa), aquí puede haber múltiples auditorías acumuladas — por ejemplo, un análisis competitivo mensual, una revisión técnica post-migración, etc. Cada auditoría contiene un reporte HTML completo que se muestra en el panel.
Lista (sin HTML).
Detalle con HTML del reporte.
Auditoría inicial
auth:sanctumEl diagnóstico rápido de una empresa: la foto de partida. Mira SSL, velocidad, robots.txt, sitemap, qué CMS usa y si hay WAF. Cada empresa tiene exactamente una y se sobreescribe si se vuelve a ejecutar (upsert). Para el análisis profundo —por URL, con puntuaciones e historial— está la Auditoría del Sitio.
auditoria_inicial_plan_free, que lleva meses inactivo: el alta gratuita prometía un reporte que nadie mandaba. El disparo a n8n se retiró del onboarding porque además dejaba la auditoría colgada en procesando esperando un callback que ya no llega.Sigue existiendo pero ya no la usa nadie. Dispara el workflow n8n, que está inactivo, así que el callback /api/agent/auditoria-inicial nunca llega y la auditoría queda en procesando. No la llames: el onboarding ya no pasa por aquí.
Auditoría del Sitio
auth:sanctumNew · 20-agoEl análisis profundo, y el que de verdad mide progreso. A diferencia de la auditoría inicial, esta guarda historial: cada corrida es un audit_run nuevo que se compara con el anterior. Lo ejecuta AgentPy, que recorre las páginas de la empresa con el seguimiento activado y las evalúa con un LLM en tres disciplinas —SEO, AEO y GEO— devolviendo puntuaciones, hallazgos priorizados y acciones concretas.
Historial de corridas de la empresa activa, la más reciente primero.
La última corrida, para pintar el estado actual sin traer el historial entero.
El detalle: resultados por URL, hallazgos ordenados por impacto y acciones por prioridad.
Lanza una auditoría nueva. Crea el registro en pending, avisa a AgentPy y devuelve el run_id sin esperar: el agente responde después por el callback.
{"ok": true, "run_id": 17, "status": "running"}
auditorias_avanzadas, como módulo o como casilla suelta; si no, devuelve 403. Hasta ahora lo único que separaba a un plan gratuito de una auditoría completa era que no le pintábamos el enlace en el menú, pero el endpoint estaba abierto a cualquiera con sesión y cada corrida gasta crawl y LLM. El candado va solo en el disparo: consultar corridas ya hechas sigue abierto, para que nadie pierda su historial si el plan cambia o caduca. El master pasa siempre, para poder dar soporte.Marca una acción como aceptada por el cliente.
Descarta una acción sugerida.
Callback de AgentPy con el resultado consolidado. También es por donde avisa si la corrida falló: manda status: "failed" con un motivo legible, en vez de dejar la corrida colgada.
Páginas / URLs
auth:sanctumRegistro de las URLs del sitio web de la empresa. Permite hacer seguimiento del posicionamiento por página individual y auditar cada URL de forma independiente. Las páginas se pueden agregar manualmente o descubrirse automáticamente mediante un crawl del sitio. Una vez registradas, el agente de auditoría SEO puede analizar cada URL y devolver recomendaciones específicas.
Registra nueva URL del sitio.
Descubrimiento automático de páginas.
Dispara auditoría SEO para una URL específica.
Productos y servicios
auth:sanctumCatálogo de lo que vende la empresa. Los agentes de IA leen este catálogo antes de generar contenido para que los artículos, posts e ideas mencionen los productos reales del negocio y no generen texto genérico. Por ejemplo, un agente de RRSS que sabe que la empresa ofrece "auditoría gratuita de 30 minutos" puede incluirlo como CTA en los posts. Sin este catálogo, el agente genera contenido sin contexto comercial.
Competidores
auth:sanctumCRUD manual de competidores de la empresa. Cada competidor tiene un campo origen que puede ser manual (creado desde el panel) o agente (enviado por un workflow externo). El panel muestra ambos tipos en la misma tabla.
Carga masiva de competidores desde un agente externo (n8n u otro workflow). Las URLs inválidas se descartan silenciosamente sin romper el batch. Soporta modo upsert (default) o insert_only con skip_existing: true. Los registros se guardan con origen = agente.
{
"empresa_id": 1,
"competitors": [
{ "url": "https://competidor.com", "name": "Competidor" },
{ "url": "otro.com" }
],
"skip_existing": false
}
CMS Integrations
auth:sanctumConexiones con el sitio web de la empresa para publicar contenido directamente desde Growth54. Por ahora soporta WordPress vía su API REST. El flujo es: el equipo guarda las credenciales WordPress de la empresa (URL, usuario, application password) → el agente generador de artículos las usa para hacer un POST directo al CMS y publicar el artículo sin intervención manual. Las credenciales siempre requieren token aunque el servidor esté en modo provisional.
Comprueba la credencial contra el sitio real (GET /wp-json/wp/v2/users/me) y devuelve verificacion. Distingue credencial inválida (401/403), API REST bloqueada (404 o HTML) y usuario sin permisos para crear entradas.
{
"verificacion": {
"ok": true,
"usuario": "wgarcia",
"roles": ["administrator"],
"message": "Conexión verificada como \"wgarcia\"."
}
}
verificacion en POST y PATCH NewGuardar una integración ya no da por buena cualquier URL con formato válido: POST /api/cms-integrations y PATCH /api/cms-integrations/{id} incluyen ahora el mismo objeto verificacion en la respuesta. El guardado se realiza igualmente aunque la comprobación falle (ok: false); antes el primer aviso de una credencial mala era un tema atascado en en_generacion.
Credenciales CMS para que el agente pueda publicar. Siempre requiere token aunque el servidor esté en modo provisional.
Agent Endpoints
auth:sanctumRegistro global de los webhooks de n8n disponibles en la plataforma. Cada agente tiene una key única (por ejemplo rrss_strategist) y una URL de webhook. Cuando el usuario hace clic en "Ejecutar agente" en el panel, el backend busca la key correspondiente en esta tabla, obtiene el webhook y lo llama. Si el webhook no está registrado o el agente está inactivo, la llamada falla con 422 y el panel muestra un mensaje indicando que hay que configurarlo en esta sección. Esto permite cambiar o actualizar los workflows de n8n sin tocar el código del backend.
rrss_schedule_alarm New · 29-jul — Alarma de publicación: la plataforma la llama al programar un post. Se siembra con AgentEndpointSeeder apuntando a schedule-alarm-g54. Ver RRSS — Posts. Las keys rrss_community_ai y rrss_sales_ai ya no son informativas: si están activas y con webhook, sus tarjetas del pipeline dejan de mostrarse como "Próximamente".GET /api/agent-endpoints devuelve ahora, junto a data, un campo catalog con las keys válidas y su etiqueta. Antes esta lista estaba escrita a mano en el frontend y se desincronizó: ofrecía paginas_discovery, que no existe en el código, y escondía landing_audit, redactor_articulos_wp y rrss_schedule_alarm, que sí se usan, de modo que no había forma de darlos de alta desde la pantalla. La fuente de verdad es AgentEndpoint::CATALOG.Devuelve { data: [...], catalog: [...] }. Cada entrada de catalog es { key, label, group }; group agrupa el desplegable (Contenido, Auditorías, Imágenes, Redes sociales, Mensajería).
key debe ser una de las del catalog; cualquier otra devuelve 422. Antes se aceptaba cualquier cadena en minúsculas.
Misma validación de key, admitiendo además la que la fila ya tenga, para que una fila antigua no quede bloqueada sin poder editarle la URL.
Activa o desactiva el agente.
Prueba la conectividad con el webhook.
rrss_distribution_ai (Distribution AI, módulo RRSS, webhook distribution-publish-g54). Publica los posts aprobados en Facebook e Instagram vía Graph API. Tiene trigger horario propio en n8n, pero al estar registrado la plataforma puede notificarlo para publicar de inmediato al aprobar un post. Se siembra con php artisan db:seed --class=AgentEndpointSeeder (idempotente).RRSS — Canales sociales
auth:sanctumEl módulo RRSS gestiona toda la operación de redes sociales de la empresa: desde la estrategia hasta la publicación y las métricas. Esta primera sección configura qué plataformas usa la empresa y con qué cuenta. Es el dato de partida que leen todos los agentes RRSS: sin canales configurados, los agentes no saben en qué plataformas generar contenido ni qué handle mencionar.
Upsert — actualiza el estado completo de los canales. Cada canal puede llevar image_config: la configuración de imagen que el cliente elige por red (no es igual para Instagram que para Facebook) y que después leen los agentes generadores. Todas sus claves son opcionales; se validan contra listas fijas y se limpia lo desconocido.
-d '{"channels":[{"platform":"instagram","active":true,"handle":"@miempresa"},{"platform":"linkedin","active":true}]}'
// Con image_config por canal:
{"channels":[{
"platform":"Instagram","is_active":true,"handle":"@miempresa","publishing_mode":"manual",
"image_config":{
"img_estilo":"realista", // realista | humanos | 3d | animado | minimalista | artistico
"img_formato":"cuadrada", // cuadrada (1:1) | vertical (4:5) | horizontal (16:9)
"img_texto_modo":"sin_texto" // sin_texto | solo_titulo | titulo_subtitulo | titulo_sub_detalles | libre
}
}]}
RRSS — Estrategia
auth:sanctumUna empresa tiene exactamente una estrategia RRSS. El PUT es upsert.
data: null si no existe aún.
New 17-jul: acepta y guarda fecha_inicio, fecha_fin (horizonte temporal de la estrategia) y direccion_deseada (a dónde llevar la marca). Antes estos campos no existían y se descartaban. Se mantiene una estrategia por empresa (se sobrescribe al regenerar).
-d '{
"fecha_inicio":"2026-07-01","fecha_fin":"2026-10-01",
"direccion_deseada":"Posicionar la marca como referente regional",
"objetivo":"Generar leads calificados","audiencia":"Dueños PYME 28-45 LATAM",
"tono":"Profesional pero cercano","frecuencia":"6 posts semanales",
"cta_principal":"Agendar llamada gratuita","embudo":"RRSS → Website → Lead → CRM",
"plataformas":["Facebook","Instagram","LinkedIn"],
"pilares":["Educación","Casos de éxito","Detrás de cámaras"],
"kpis":["Alcance orgánico","Engagement","Leads generados"]
}'
New · 14-ago (#68 / #69) Dos campos más, ambos opcionales: frecuencia_estructurada — la frecuencia legible por máquina, {"Facebook":2,"Instagram":1} = publicaciones por semana (el texto libre de frecuencia se conserva tal cual) — y link_principal, la URL a la que lleva el clic de un post; tiene que ser una URL válida. No confundir link_principal con cta_principal: la primera es el destino del clic, la segunda es la frase de llamada a la acción.
New 29-jul (B-17): se agregaron 6 campos que el Strategist AI ya generaba y que el guardado descartaba en silencio (mismo caso que fecha_inicio/fecha_fin/direccion_deseada el 17-jul): resumen_estrategico (texto), keyword_strategy, mapa_preguntas_aeo, faqs, clusters y plan_publicacion. Los 5 últimos se guardan como JSON: se prefiere mandar arrays, pero también se acepta texto plano. Aplican igual a POST /api/agent/rrss/estrategia, que es la vía que usa el agente.
-d '{
"resumen_estrategico":"Posicionar la marca en logística regional",
"keyword_strategy":[{"kw":"exportar a colombia","volumen":1200,"intencion":"comercial"}],
"mapa_preguntas_aeo":["¿Cuánto cuesta exportar a Colombia?"],
"faqs":[{"q":"¿Hacen envíos el mismo día?","a":"Sí, según zona"}],
"clusters":["logistica","aduanas"],
"plan_publicacion":[{"semana":1,"posts":3,"pilar":"Educación"}]
}'
RRSS — Ideas
auth:sanctumEl banco de ideas es el primer paso del pipeline de contenido social. Una idea es simplemente un tema propuesto: "5 errores que cometen las PYMEs en Instagram". Puede crearla manualmente el equipo, o puede generarlas el agente Content AI en bulk. El equipo revisa el banco y aprueba las que le parecen buenas — las aprobadas son las que el agente luego convierte en posts completos. Las descartadas quedan registradas para no proponer el mismo tema de nuevo.
Filtros: ?status=aprobado · ?platform=Instagram
-d '{"topic":"5 errores de PYMEs en Instagram","platform":"Instagram","keyword":"marketing PYME"}'
Valores admitidos: idea · aprobado · descartado · usada. Ej. {"status":"aprobado"}
usada New · 19-ago — Marca una idea que ya generó un post. Hasta ahora la única forma de sacarla de la lista era descartado, que significa lo contrario: que se rechazó. Con los dos mezclados no había manera de saber si una idea se aprovechó o se tiró. En el panel se pinta distinto (azul «Ya usada», no rojo) y bloquea aprobar, descartar y regenerar, porque a esas alturas ya no significan nada. Quien genera el post debería dejar la idea en usada; cualquier otro valor sigue funcionando igual que antes.Edita una idea ya creada. Pensado para corregir la plataforma que asignó el Strategist AI desde el panel (Banco de Ideas), sin tener que borrar y recrear. Campos opcionales: topic, platform, keyword, New angulo.
New angulo permite actualizar la justificación editorial junto con el título cuando se regenera una idea conservando su keyword. Antes se descartaba en silencio.
-d '{"platform":"Ambas"}'
platform válido: Facebook | Instagram | Ambas.
RRSS — Posts
auth:sanctumUn post es el contenido final listo para publicar. Cada post tiene copy adaptado para cada plataforma (Instagram, Facebook, LinkedIn), hooks alternativos para el inicio del texto, un CTA específico y opcionalmente una imagen o prompt para generarla con IA. El ciclo de vida es: el agente genera el borrador → el equipo lo revisa en el panel → lo aprueba → le asigna una fecha → queda programado. Cuando se publica, el sistema registra automáticamente quién lo aprobó y la fecha de publicación.
Filtros: ?status=borrador · ?platform=LinkedIn
Edita el contenido de un post desde el editor del panel. Campos opcionales: tema, platform, copy_instagram, copy_facebook, copy_linkedin, imagen_url, New video_url, hook, cta. El hook es un texto único (la primera línea que capta la atención); se persiste internamente en el array hooks.
-d '{"hook":"¿Sabías que el 80%...","cta":"Cotiza hoy","imagen_url":"https://.../post.png"}'
New 29-jul (B-14) — video_url, habilita Reels. Antes no existía la columna y el campo se descartaba en silencio, así que un post no podía llevar video adjunto. Se acepta en POST/PUT de posts y en POST /api/agent/rrss/posts, y se devuelve en GET /rrss/posts y GET /rrss/posts/to-publish — sin alias, se llama igual dentro y fuera. Debe ser una URL directa al archivo de video.
Elimina un post desde el panel. Solo se permiten posts en estado borrador — un post aprobado, programado o publicado devuelve 422. Útil para limpiar borradores duplicados generados en pruebas del Content AI.
Al aprobar → guarda approved_by_user_id. Al publicar → guarda fecha_publicada.
-d '{"status":"aprobado"}'
Programa post. Cambia status a programado. La fecha debe ser futura. New La hora se interpreta en la zona horaria de la empresa (companies.timezone); se guarda en UTC. Si la empresa no tiene zona, se asume UTC. Verifica que la empresa tenga su zona horaria establecida en Editar datos de la empresa → País / Zona horaria.
-d '{"fecha_programada":"2026-05-20T10:00:00"}'
New 29-jul (B-12) — al programar se avisa a la alarma de publicación. En el mismo momento en que se guarda la fecha, el backend hace una llamada única a la key rrss_schedule_alarm de agent_endpoints. Es dispara y olvida: el resultado no afecta la respuesta, y si el webhook falla o no está configurado el post queda programado igual (queda anotado en el log). Reemplaza al puente que revisaba cada 2 minutos.
// Lo que recibe el webhook:
{"post_id":40,"company_id":34,"fecha_programada":"2026-08-03T20:36:34Z"}
No es solo este endpoint. El aviso sale por los 4 caminos que dejan un post programado: este schedule, POST /api/rrss/posts creado ya con status:"programado", PUT /api/rrss/posts/{id} (el composer del calendario edita fecha y estado juntos) y PUT /api/rrss/posts/{id}/status.
Cuándo NO se avisa — para no mandar alarmas duplicadas ni inútiles: si solo se editó el contenido de un post ya programado (no cambió ni la fecha ni el estado); si la fecha quedó en el pasado; si el post no está en programado; o si la key rrss_schedule_alarm no está activa. Reprogramar a otra fecha sí vuelve a avisar, con la fecha nueva.
Sube una imagen desde el equipo del usuario para adjuntarla a un post (el editor del panel la usa cuando el operador arrastra o elige un archivo, como alternativa a pegar una URL). El backend optimiza la imagen: la reescala a un máximo de 1600px de ancho, la aplana sobre fondo blanco y la recomprime a JPEG (calidad 80). Devuelve una URL pública lista para guardar en imagen_url del post. multipart/form-data, campo file, máx. 10 MB, solo imágenes.
curl -s -X POST "https://api.growth54.com/api/images/upload" \
-H "Authorization: Bearer <token>" \
-F "file=@/ruta/imagen.png"
// Respuesta 201:
{"ok":true,"url":"https://.../storage/ai-images/<uuid>.jpg","bytes":184320}
RRSS — Métricas
auth:sanctumNew · 25-agoEl cierre del ciclo: después de publicar, el agente Analytics AI recoge los números reales de cada plataforma (alcance, impresiones, engagement, clics a perfil, leads generados) y los carga en la base de datos. La vista de métricas del panel muestra estos datos agrupados por plataforma y calcula promedios. También muestra el top 10 de posts por engagement, lo que permite al equipo identificar qué tipo de contenido funciona mejor para esa empresa.
Resumen de cuenta por plataforma. Filtros: ?platform=Instagram · ?from=2026-05-01 · ?to=2026-05-31
// Respuesta:
{"data":[{"platform":"Instagram","alcance_total":12430,"impresiones_total":38000,
"engagement_total":584,"engagement_rate_avg":4.7,
"guardados_total":142,"compartidos_total":38, // New · 25-ago
"visitas_perfil_total":218,"seguidores_nuevos_total":52, // New · 25-ago
"clics_total":384,"leads_total":17}]}
New 25-ago (#64 y #65): cuatro campos nuevos en la respuesta. guardados_total, compartidos_total y visitas_perfil_total son señales de demanda — interés real que todavía no es un lead — y seguidores_nuevos_total es crecimiento de audiencia. Se escriben desde POST /api/agent/rrss/metricas y desde el panel. Son opcionales: lo que no se manda queda en 0, y las filas viejas valen 0 porque no había dónde guardarlos.
Cambió el 17-jul: (1) los números salen como número; antes eran texto ("5000") y el panel los concatenaba en vez de sumarlos (mostraba 050003200). (2) El agregado ahora excluye las filas con social_post_id: guardar métricas de un post ya no infla el total de la cuenta. Esas métricas siguen en top-posts.
Si el GET "no refleja lo que acabas de guardar": no es caché. Devuelve la suma del período, no el último valor — 16 sobre un histórico de 5000 da 5016. Además el guardado por agente usa el company_id del body y esta lectura usa la empresa activa de la sesión: si no coinciden, miras otra empresa.
Top 10 posts por engagement.
New 17-jul: métricas de cuenta agrupadas por número de semana relativo al fecha_inicio de la estrategia vigente. Sin estrategia con fecha responde has_strategy_dates:false y data:[]. Filtro opcional ?platform=Instagram.
// Respuesta:
{"data":[{"semana":1,"alcance_total":1000,"impresiones_total":3200,"engagement_total":84,
"guardados_total":12,"compartidos_total":7, // New · 25-ago
"visitas_perfil_total":40,"seguidores_nuevos_total":9, // New · 25-ago
"clics_total":50,"leads_total":5,
"calificados_total":3,"deals_total":8}], // New · 25-ago
"has_strategy_dates":true,"fecha_inicio":"2026-07-01","fecha_fin":"2026-10-01"}
New 25-ago (#66): calificados_total y deals_total cierran el embudo por semana. Se calculan aquí y no derivándolos de /api/crm/board + /api/whatsapp/conversations: eso duplica la lógica en cada consumidor y se desincroniza en cuanto cambie un criterio.
Los dos criterios, para que no haya sorpresas: la semana de un deal es la de su created_at (cuándo entró), no la de su último cambio de etapa — es una vista de cohorte. Y calificado = temperatura:"alto", el «caliente» del panel; no se usa la etapa qualifying porque un deal que ya avanzó a proposal saldría de la cuenta.
New El eje de semanas ahora es la unión de las que tienen métricas y las que tienen deals: una semana con deals pero sin métricas cargadas ya aparece, con los totales sociales en 0. No se rellena hasta fecha_fin a propósito — una estrategia a 6 meses devolvería decenas de semanas vacías desde el primer día. El filtro ?platform se traduce a origen para el CRM (Facebook → facebook); con cualquier otro valor los deals no se filtran, porque un deal tiene origen, no plataforma.
New 25-ago (#71). Métricas era el único recurso de RRSS sin forma de deshacer una carga: un dato de prueba quedaba sumado al histórico para siempre. Acota por la empresa activa además del id, así que una fila de otra empresa da 404 y no 403 — desde fuera no se distingue «no existe» de «no es tuya».
// → 200
{"ok":true,"deleted":1}
New 25-ago (#71). Borrado por lote para limpiar una carga entera. Filtros opcionales platform, period_start (borra desde) y period_end (borra hasta), combinables.
Exige al menos un filtro. Sin ninguno responde 422 y no borra nada: un DELETE con el cuerpo vacío arrasaría con el histórico completo de la empresa activa de un solo golpe.
-d '{"platform":"Facebook","period_start":"2026-08-01","period_end":"2026-08-31"}'
// → 200
{"ok":true,"deleted":3}
RRSS — Reportes HTML
auth:sanctumNew · 29-julNew 29-jul (B-10). El Analytics AI genera un reporte de resultados en HTML; antes no había dónde guardarlo, así que el agente tenía que volver a generarlo cada vez que alguien quería verlo. Ahora se guarda en G54 y el panel lo lee del histórico. El agente escribe con POST /api/agent/rrss/reportes; estas 3 rutas son las del panel.
Histórico de la empresa activa, del más reciente al más viejo. No devuelve el html a propósito: un reporte mensual pesa cientos de KB y el listado cargaría megas por gusto.
// Respuesta:
{"data":[{"id":7,"titulo":"Reporte julio 2026","periodo_inicio":"2026-07-01",
"periodo_fin":"2026-07-31","modo":"completo","generated_by":"n8n","updated_at":"..."}]}
El reporte con su html completo, para mostrarlo o compartirlo. Un reporte de otra empresa devuelve 403.
Envía el reporte por correo a uno o varios destinatarios (máximo 20). Usa el SMTP de la propia empresa, el que ya está configurado en Ajustes → Correo electrónico, así el cliente lo recibe desde la dirección de su marca y no desde una de Growth54.
-d '{"destinatarios":["cliente@empresa.com","socio@empresa.com"],
"mensaje":"Hola, te comparto el reporte del mes."}'
// 200: {"ok":true,"message":"Reporte enviado."}
// 422: {"ok":false,"message":"Falta configurar el correo de la empresa..."}
El reporte viaja de dos formas a la vez: como cuerpo HTML (se ve al abrir el correo) y como archivo .html adjunto. El adjunto está porque Gmail y Outlook recortan CSS moderno, y así el reporte siempre se puede abrir intacto en el navegador. Se incluye también una versión en texto plano y una copia oculta al remitente, para que quede constancia en su bandeja.
Errores: 422 si la empresa no tiene correo configurado o activo, o si el servidor SMTP rechaza el envío (el mensaje incluye el motivo que devolvió el servidor). 403 si el reporte es de otra empresa. Un destinatario mal formado devuelve 422 de validación.
Borra un reporte del histórico. Un reporte de otra empresa devuelve 403.
RRSS — Pipeline de agentes
auth:sanctumNew · 29-julFuente de datos de las tarjetas del Dashboard de Agentes del panel (Strategist, Content, Distribution, Community, Sales, Analytics). Cada paso combina dos cosas distintas: si el agente existe y está configurado (fila activa con webhook_url en agent_endpoints) y qué produjo realmente para la empresa activa. Se documenta ahora porque no había forma de saber desde fuera de dónde salían esos números.
// Respuesta (un objeto por paso):
{"data":[{"key":"rrss_analytics","nombre":"Analytics AI","disponible":true,
"configurado":true,"estado":"listo","valor":"129.671","unidad":"de alcance acumulado",
"detalle":"2 lecturas de métricas.","accion":"analytics","ultima_actividad":"..."}],
"resumen":{"listos":4,"total_disponibles":6}}
estado: listo (ya produjo algo) · pendiente (configurado, sin usar) · sin_configurar (falta el webhook_url) · proximamente (el agente no existe todavía en agent_endpoints).
New Community AI y Sales AI dejan de estar clavados en "Próximamente". Antes su estado estaba escrito a mano en el código; ahora se deriva de agent_endpoints igual que el resto: si existen las keys rrss_community_ai / rrss_sales_ai activas y con webhook, las tarjetas pasan a estado real. Su actividad se mide con los contactos (wa_contacts) y deals (crm_deals) cuyo origen es facebook o instagram.
New Analytics AI mostraba 0 de alcance acumulado teniendo métricas reales. Contaba social_post_id distintos, y las métricas de cuenta se guardan con ese campo en NULL — COUNT(DISTINCT NULL) da 0 y escondía el total. Ahora cuenta filas y agrega el alcance con el mismo criterio que GET /api/rrss/metricas (solo filas de cuenta, para no duplicar), con las filas por post como respaldo si no hay ninguna de cuenta. Los dos números ahora coinciden.
RRSS — Triggers de agentes
auth:sanctumCuando el usuario hace clic en "Ejecutar agente" en el panel, el frontend llama uno de estos endpoints. El backend busca la key del agente en la tabla agent_endpoints, obtiene la URL del webhook de n8n y lo dispara pasándole el company_id. Si el webhook no está configurado, responde con 422 y un mensaje indicando que hay que configurarlo en Panel › Configuración › Agentes. El agente trabaja de forma asíncrona: el webhook responde inmediatamente con 200 y n8n procesa en segundo plano.
Key: rrss_strategist
New · 6-ago (I-1) Acepta el brief del cliente, ambos campos opcionales: horizonte_meses (solo 1, 3, 6 u 8; otro valor da 422) y direccion_deseada (texto libre, máx. 2000). El panel los pide en un modal antes de disparar el agente. Cuando llega horizonte_meses, el backend calcula además el bloque periodo con las fechas ya resueltas, para que el agente no tenga que hacer aritmética de calendario. Sin brief el payload sale idéntico a como salía antes.
New · 14-ago (#69) Tercer campo del brief, también opcional: link_principal, la URL a la que mandar a quien haga clic en un post. Debe ser una URL válida (si no, 422). El payload lo lleva siempre resuelto: si el cliente no lo indica se cae al website real de su perfil de empresa, y si tampoco hay website viaja null. Nunca un valor sugerido ni generado. link_origen dice de dónde salió — cliente, perfil_empresa o ninguno; con ninguno el agente no debe publicar ningún link. Lo que elige el cliente se guarda en la estrategia sin esperar al agente.
Ojo, no confundir con cta_principal: esa es la frase de llamada a la acción ("Agendar llamada de diagnóstico gratuita"). link_principal es el destino del clic. Son dos columnas distintas.
-d '{"horizonte_meses": 6, "direccion_deseada": "Posicionar la marca como referente",
"link_principal": "https://miempresa.com/contacto"}'
# lo que recibe n8n:
{
"company_id": 19,
"trigger": {"source": "ui", "user_id": 1},
"horizonte_meses": 6,
"periodo": {"fecha_inicio": "2026-08-07", "fecha_fin": "2027-02-07"},
"direccion_deseada": "Posicionar la marca como referente",
"link_principal": "https://miempresa.com/contacto",
"link_origen": "cliente"
}
Key: rrss_content_ai. mode: ideas | post. idea_id se reenvía a n8n.
New 17-jul (I-5): el botón "Regenerar" de una idea llama a este endpoint con mode:"ideas" y el idea_id de esa idea. Acción para Sol: el flujo ideas-ai-g54 debe, cuando llega idea_id, regenerar esa idea conservando su keyword (en vez de generar un lote nuevo).
New 29-jul — por qué ideas-ai-g54 no registra ejecuciones: el botón sí dispara. El panel nunca llama a un webhook de n8n directamente: llama a este endpoint y el backend reenvía a la URL guardada en agent_endpoints para la key rrss_content_ai. Si esa fila apunta a otro webhook, el flujo ideas-ai-g54 no ve nada. Además el contrato es el de arriba (mode/ideas), no modo/regenerar: los nombres deben coincidir. Para conectarlo hay que alinear ambas cosas — la URL de la fila y los nombres de campos.
-d '{"mode":"ideas","idea_id":null}' // genera lote nuevo
-d '{"mode":"ideas","idea_id":5}' // regenera la idea 5 con su keyword
-d '{"mode":"post","idea_id":5}'
Key: rrss_analytics
n8n — Lectura general
Bearer N8N_TOKENEstos endpoints existen para que los workflows de n8n puedan leer el contexto de la empresa antes de generar contenido. Un agente que va a escribir artículos necesita saber la industria de la empresa, sus keywords, sus productos. Un agente RRSS necesita saber en qué plataformas está activa la empresa y cuál es su tono de comunicación. Todos son de solo lectura — los agentes leen desde aquí y escriben los resultados a través de los endpoints /api/agent/*.
Lista todas las empresas. New 25-ago — tres filtros para los agentes con horario propio. Un flujo que corre cada semana y recorre esta lista intentaría trabajar también para empresas dadas de baja, sin CMS conectado o sin ninguna red, y fallaría una vez por cada una — llenando el historial de ejecuciones de errores que no son errores.
?activas=1 // solo is_active = true
?con_cms=wordpress // solo con esa integración de CMS activa
?con_redes=1 // solo con al menos una red conectada y activa
// se combinan:
GET /api/n8n/companies?activas=1&con_cms=wordpress&con_redes=1
Son filtros de hecho, no de plan contratado: dicen qué se puede hacer hoy con los datos que hay, no a qué tiene derecho la empresa. Sin filtros devuelve todo, igual que siempre — el cambio es aditivo y no rompe a quien ya lo consume.
Es el punto de partida de un agente multiempresa: se pide la lista filtrada, y por cada empresa se piden su perfil, su CMS y sus redes con los endpoints de abajo. Ningún identificador de cliente debería quedar escrito dentro del workflow.
Perfil de empresa (sin datos sensibles).
Filtros: ?estado=activa · ?origen=manual
Solo keywords sin artículo.
Detalle con HTML del reporte.
n8n — Lectura RRSS
Bearer N8N_TOKENBase: /api/n8n/companies/{id}/rrss/
Canales activos con handle, objetivo, frecuencia y rol en el embudo.
null si no existe.
New · 14-ago (#69) Devuelve además link_resuelto y link_origen, ya calculados de este lado: el agente no tiene que decidir a dónde manda el clic. Es el link_principal que eligió el cliente o, si no eligió, el website del perfil de empresa. link_origen vale estrategia, perfil_empresa o ninguno; con ninguno no hay ningún dato real que usar y no se debe publicar link — nunca uno inventado. cta_principal sigue siendo la frase, no la URL.
New · 14-ago (#68) Devuelve también frecuencia_estructurada: la misma frecuencia legible por máquina, {"Facebook":2,"Instagram":1} = publicaciones por semana. El texto libre de frecuencia se mantiene tal cual. Puede venir null si la frecuencia guardada no se pudo interpretar.
{
"data": {
"frecuencia": "8 posts semanales: 5 Instagram, 3 Facebook",
"frecuencia_estructurada": {"Facebook": 3, "Instagram": 5},
"cta_principal": "Agendar llamada de diagnóstico gratuita",
"link_principal": null,
"link_resuelto": "https://miempresa.com/",
"link_origen": "perfil_empresa"
}
}
/api/n8n/companies/{id}/social-targets con N8N_TOKEN. Ahora cuelga de /api/agent/ y pide Authorization: Bearer <AGENT_TOKEN>. El motivo: es la única lectura que entrega access token vivos de Meta, y el grupo /api/n8n/ queda abierto mientras N8N_API_TOKEN esté vacío. La respuesta es idéntica; solo cambian la ruta y el header.New 25-ago (#82) — las redes conectadas de una empresa, sin depender de la cola de publicación. Hasta ahora los identificadores y tokens solo salían por GET /rrss/posts/to-publish, y solo pegados a un post programado: un flujo que publica por su cuenta —el de blog— o que solo quiere leer métricas no tenía forma de saber a qué página escribir. Ojo a la ruta: cuelga de /companies/{id}/, no de /companies/{id}/rrss/.
{"data":[
{"platform":"Facebook","label":"Mi Página","target_id":"1713965015486703",
"page_id":"1713965015486703","instagram_account_id":null,
"access_token":"EAA...","expires_at":null,"expirado":false,"listo":true},
{"platform":"Instagram","label":"Mi IG","target_id":"26563071519994138",
"page_id":"1713965015486703","instagram_account_id":"26563071519994138",
"access_token":"IGAA...","expires_at":null,"expirado":false,"listo":true},
{"platform":"LinkedIn","label":"Mi Empresa SL","target_id":"112205115",
"page_id":"112205115","instagram_account_id":null,
"access_token":"AQV...","expires_at":"2027-08-25T00:00:00+00:00",
"expirado":false,"listo":true}
]}
Usa target_id y olvídate del resto. Cada red publica contra un identificador distinto —Instagram contra el instagram_account_id, Facebook contra el page_id, y LinkedIn guarda el id de su organización también en page_id—. target_id ya trae el que corresponde a esa plataforma; page_id e instagram_account_id quedan por si necesitas distinguirlos.
Conectada no es lo mismo que utilizable. Por eso viene listo: es false si el token caducó (expirado:true) o si la fila se cargó a mano y quedó sin identificador. Comprueba listo antes de publicar — si no, Meta o LinkedIn devuelven un error que no dice nada útil.
Una empresa sin nada conectado devuelve {"data":[]} con 200, no 404: no tener redes es una respuesta válida, no un error. Las conexiones inactivas no aparecen. Devuelve las tres vías por igual, sin importar si el token entró por OAuth o cargado a mano.
Solo ideas con status aprobado.
Posts con status aprobado o programado.
image_url New — La columna interna se llama imagen_url, pero GET /rrss/posts y GET /rrss/posts/to-publish ahora devuelven también el campo image_url (mismo valor) para los agentes. La imagen es obligatoria para publicar en Instagram; si viene vacía, el post solo puede publicarse como texto en Facebook.video_url New · 29-jul — Ambas lecturas devuelven además video_url cuando el post lleva video adjunto (Reels). No tiene alias: se llama igual dentro y fuera. Si viene vacío, el post se publica como imagen o texto según corresponda.Traduce un page_id de Meta a la empresa dueña. Meta manda el page_id en el webhook pero no el company_id; esta es la vía para resolverlo (Community AI multiempresa). Devuelve 404 si el page_id no está registrado.
{"company_id": 19, "fb_access_token": "EAA...", "ig_user_id": "17841446392201293", "ig_access_token": "IGAA..."}
ig_access_token añadido New · 19-ago — Con la migración a Instagram API with Instagram Login, el token de Instagram ya no es el mismo que el de Facebook. Este endpoint devuelve ahora los dos por separado: fb_access_token sale de la fila de Facebook y ig_access_token de la fila de Instagram — la misma de donde ya salía ig_user_id, con el mismo respaldo si esa fila se cargó a mano sin page_id. Si la empresa no tiene Instagram conectado, ig_user_id e ig_access_token vuelven null los dos. Usa ig_access_token para responder mensajes directos de Instagram; con el de Facebook, Meta rechaza la llamada.ig_user_id ya no vuelve vacío New · 6-ago — Un mismo page_id vive en dos filas de company_social_tokens (el unique de la tabla es empresa+plataforma): la de Facebook, sin instagram_account_id, y la de Instagram, con él. La resolución tomaba una sola fila sin ordenar y casi siempre caía en la de Facebook, devolviendo ig_user_id: null aunque el dato estuviera guardado. Ahora se busca en todas las filas activas de ese page_id y, si la cuenta de Instagram se cargó a mano sin page_id, se resuelve dentro de la misma empresa. fb_access_token sigue saliendo de la fila de Facebook.Agent — Escritura general
Bearer AGENT_TOKENUna vez que el agente termina su trabajo (generar keywords, escribir artículos, crear auditorías), usa estos endpoints para guardar los resultados en Growth54. Se identifican por el AGENT_API_TOKEN, distinto al token de usuario, lo que significa que el agente puede escribir datos sin tener una sesión de usuario activa. Todos los registros creados por esta vía quedan marcados con generated_by = 'n8n' para distinguirlos de los creados manualmente.
Inserta o actualiza keywords en bulk. skip_existing: true no toca las existentes.
-d '{"empresa_id":1,"skip_existing":true,"keywords":[
{"keyword":"abogado laboral","dificultad_nivel":"media","intencion":"comercial","fuente":"Google"}
]}'
-d '{"empresa_id":1,"articulos":[{"keyword_id":10,"titulo":"Guía de contrato","contenido_html":"<h1>...","estado":"borrador"}]}'
Guarda o actualiza la auditoría inicial (upsert por empresa).
Guarda los temas que genera content-decision. Ya no rechaza la tanda entera: como el contenido lo escribe un LLM, los campos se sanean en vez de validarse en estricto. Un keyword_id inexistente o de otra empresa se ignora y la keyword se resuelve desde primary_keyword; un source o intent fuera del enum pasa a agent/null; priority y score se acotan a 0-100; title y angle se recortan a 255. Solo se salta el tema que venga sin título, y se informa en descartados.
{
"ok": true,
"created": 9,
"descartados": ["tema #3: sin título"]
}
Agent — Escritura RRSS
Bearer AGENT_TOKENTodos los registros creados quedan con generated_by = 'n8n' automáticamente.
New 17-jul: acepta fecha_inicio, fecha_fin y direccion_deseada (igual que el PUT del panel). Sigue haciendo updateOrCreate por empresa.
New · 14-ago (#68 / #69) Dos campos más, ambos opcionales: frecuencia_estructurada — la frecuencia legible por máquina, {"Facebook":2,"Instagram":1} = publicaciones por semana (el texto libre de frecuencia se conserva tal cual) — y link_principal, la URL a la que lleva el clic de un post; tiene que ser una URL válida. No confundir link_principal con cta_principal: la primera es el destino del clic, la segunda es la frase de llamada a la acción.
New 29-jul (B-17): acepta también resumen_estrategico, keyword_strategy, mapa_preguntas_aeo, faqs, clusters y plan_publicacion. Ver el ejemplo completo en RRSS — Estrategia.
-d '{"company_id":3,"fecha_inicio":"2026-07-01","fecha_fin":"2026-10-01",
"direccion_deseada":"...","objetivo":"...","audiencia":"...","tono":"...",
"plataformas":["Instagram","LinkedIn"],"pilares":["Educación"],"kpis":["Alcance"]}'
Bulk. Respuesta: {"ok":true,"created":N}
New Ahora acepta y guarda angulo (justificación de la idea, texto — se muestra al cliente bajo el tema) y es_tendencia (booleano — pinta el badge "Tendencia" en el panel). Ambos opcionales; si no se mandan, la idea queda sin justificación y sin marca.
-d '{"company_id":3,"ideas":[{"topic":"5 errores PYME","platform":"Instagram","keyword":"marketing","angulo":"Aborda el dolor #1 de las PYMEs este trimestre","es_tendencia":true}]}'
Bulk. Llegan con status = borrador.
-d '{"company_id":3,"posts":[{
"tema":"5 errores PYME","platform":"Instagram","social_idea_id":5,
"copy_instagram":"¿Tu negocio está en IG pero no consigue resultados?\n...",
"copy_linkedin":"En 2026, producir sin estrategia es el error más común...",
"hook":"¿Sabías que el 80% de las PYMEs comete este error?",
"cta":"Comenta o DM para auditoría gratuita.",
"imagen_prompt":"Minimalist flat design 5 warning signs PYME"
}]}'
Campo hook (singular) New — El Content AI puede enviar hook como un solo texto (la primera línea del post). Se guarda y se edita igual desde el panel. Sigue aceptándose hooks (array) por compatibilidad, pero se recomienda hook.
New 29-jul (B-14): acepta posts.*.video_url para posts con video (Reels). Ver RRSS — Posts.
New 29-jul (B-10). Guarda el reporte HTML de un período para que quede disponible en el panel. Es upsert por company_id + periodo_inicio + periodo_fin + modo: regenerar el reporte de un mes reemplaza el anterior en vez de acumular copias. Devuelve 201 si lo creó y 200 si lo actualizó.
-d '{"company_id":3,"titulo":"Reporte julio 2026",
"periodo_inicio":"2026-07-01","periodo_fin":"2026-07-31",
"modo":"completo","html":"<html><body>...</body></html>"}'
// Respuesta 201 (no devuelve el html de vuelta):
{"ok":true,"data":{"id":7,"titulo":"Reporte julio 2026",
"periodo_inicio":"2026-07-01","periodo_fin":"2026-07-31","modo":"completo"}}
Obligatorios: company_id, titulo, periodo_inicio, periodo_fin, html. modo es opcional y sirve para guardar varias versiones del mismo período (por ejemplo resumen y completo) sin que una sobrescriba a la otra. Un periodo_fin anterior al inicio devuelve 422. El HTML se guarda en longText: probado con 360 KB sin truncar. Las rutas de lectura están en RRSS — Reportes HTML.
Borra un post (limpieza de duplicados de pruebas). Solo borradores: un post aprobado/programado/publicado devuelve 422 con {"ok":false,"error":"..."}.
Lo llama el Distribution AI después de intentar publicar. Cambió el 17-jul: antes el external_post_id se validaba pero nunca se guardaba, y un fallo dejaba el post atascado en programado sin aviso.
// Éxito:
-d '{"external_post_id":"17912345678901234",
"link_url":"https://www.instagram.com/p/ABC123/"}'
// → status=publicado · guarda post_id_platform + link_url · limpia error_message
// → {"ok":true,"data":{...post...}}
// Fallo:
-d '{"error_message":"Instagram requiere imagen: el post no tiene imagen_url"}'
// → el post VUELVE a `aprobado` y guarda el motivo (el cliente lo ve en el panel)
// → {"ok":false,"message":"..."}
link_url es un parámetro nuevo, opcional. Un post ya publicado no se revierte. Enviar external_post_id:"" no borra un ID ya guardado; llamar dos veces es seguro.
⚠️ Para reportar fallos usa esta ruta. PUT /api/rrss/posts/{id}/status no acepta el AGENT_TOKEN (da 401) y seguirá así: resuelve la empresa desde el usuario de la sesión, y un agente no tiene usuario. Si reportas el fallo por ahí, el post se queda atascado y el cliente no ve nada.
Acumula, no sobrescribe: cada llamada inserta una fila nueva con su período. Manda social_post_id para métricas de un post; omítelo para las de cuenta. New Desde el 17-jul las métricas por post ya no inflan el agregado de cuenta.
-d '{"company_id":3,"metricas":[{
"platform":"Instagram","period_start":"2026-05-01","period_end":"2026-05-31",
"social_post_id":12,"alcance":3420,"impresiones":10500,
"engagement_count":212,"engagement_rate":6.2,"clics":89,"leads":4,
"guardados":31,"compartidos":9, // New · 25-ago
"visitas_perfil":74,"seguidores_nuevos":18 // New · 25-ago
}]}'
New 25-ago (#64 y #65): cuatro campos nuevos, los cuatro opcionales. Lo que no se manda queda en 0, así que no hay que tocar nada en un flujo que ya funciona. Salen sumados en GET /api/rrss/metricas y en /weekly.
CRM — Deals
auth:sanctumNewEl pipeline comercial. Un deal es la ficha de un negocio: puede nacer de un lead de WhatsApp, del formulario web o a mano. Documentado el 17-jul a raíz de dos reportes del equipo de agentes.
Corregido el 17-jul: telefono y origen ya se pueden editar. Antes la ruta respondía 200 pero ignoraba esos dos campos en silencio (no estaban en la validación), así que un dato mal guardado ahí era incorregible.
-d '{"telefono":"+593999888777","origen":"agente"}'
// → 200 y ahora sí persiste
// Campos editables:
// nombre · etapa · valor_estimado · responsable · lead_score
// temperatura · notas · posicion · telefono (New) · origen (New)
Solo afecta a deals de la empresa activa; el de otra empresa devuelve 404.
Valores válidos de origen: whatsapp · web · manual · agente · importacion · New facebook · instagram. Es un enum de base de datos. 17-jul (B-5): se agregaron facebook e instagram vía migración; antes no existían. En contactos de WhatsApp el enum es distinto: rrss · web · qr · ai · direct · New facebook · instagram.
New 17-jul: un origen fuera del enum, o un telefono de más de 30 caracteres, ahora devuelven 422 con el campo señalado. Antes reventaban con 500 y el SQL crudo (1265 Data truncated / 1406 Data too long).
El tablero completo de la empresa activa, agrupado por etapa. Devuelve siempre las seis etapas, también las que no tienen ningún deal.
{
"new": [ {...}, {...} ],
"qualifying": [],
"proposal": [],
"negotiation": [],
"closed": [],
"lost": [ {...} ]
}
Corregido el 20-ago: antes se agrupaba y ya está, así que una etapa sin deals no aparecía en absoluto — ni como array vacío. Quien consumía esto no podía distinguir «esta etapa está vacía» de «esta etapa no existe», y tenía que asumir que la ausencia significaba cero. Ahora las seis claves están siempre. No pagina: devuelve todos los deals de la empresa.
Agrega una entrada al historial de actividades del deal: llamadas, notas, mensajes, lo que sea que deje constancia de un contacto. Es lo que se ve en la pestaña «CRM» de la ficha del lead.
-d '{"tipo":"whatsapp","contenido":"Llamada por WhatsApp registrada desde la ficha."}'
// → 201 con la actividad creada
// tipo (obligatorio): nota · llamada · whatsapp · email · sistema
// contenido (obligatorio): texto libre
// autor: se rellena solo con el nombre del usuario de la sesion
El endpoint no es nuevo, la documentación sí. Existía sin estar en esta guía, y por eso se pidió como si faltara. Para agentes n8n el equivalente es POST /api/agent/crm/leads/{deal}/activities, con AGENT_TOKEN — ojo al /crm/, que en la primera versión de esta nota se nos cayó y mandaba a un 404. Un deal de otra empresa devuelve 404.
New 25-ago — el campo origen acepta cuatro valores más. Los tres sitios donde se valida (el panel, la edición y esta ruta de agentes) tenían listas distintas, y ninguna coincidía con la columna real: esta ruta rechazaba facebook, instagram, telegram y email aunque la base los aceptaba desde julio. Ahora las tres leen la misma lista.
origen: whatsapp · web · manual · agente · importacion
facebook · instagram · telegram · email
prospeccion // New · 25-ago
prospeccion es para el lead que salimos a buscar — el agente que rastrea negocios por zona y rubro—, y por eso no se mezcla con agente. La diferencia importa: un lead entrante y uno prospectado en frío no valen lo mismo en el embudo, y solo el segundo necesita consentimiento antes de escribirle.
Un valor fuera de la lista devuelve 422, no se guarda en silencio.
Agentes n8n — Config inicial
Todos los workflows empiezan con un nodo Set llamado Config.
BASE_URL = https://api.growth54.com ← producción
https://xxx.ngrok-free.app ← local con ngrok
AGENT_TOKEN = (AGENT_API_TOKEN del .env)
N8N_TOKEN = (N8N_API_TOKEN del .env)
COMPANY_ID = {{ $json.company_id }} ← viene del trigger del panel
Authorization: Bearer <N8N_TOKEN>Escritura →
Authorization: Bearer <AGENT_TOKEN>
| Agente | Key |
|---|---|
| Strategist AI | rrss_strategist |
| Content AI (ideas + post) | rrss_content_ai |
| Analytics AI | rrss_analytics |
Strategist AI
rrss_strategistLee perfil y canales, genera estrategia RRSS completa, la guarda. El equipo puede editarla desde el panel.
company_idGET /api/n8n/companies/{id} — nombre, industria, paísGET /api/n8n/companies/{id}/rrss/canalesGET /api/n8n/companies/{id}/rrss/estrategia — puede ser nullPOST /api/agent/rrss/estrategiaEres un estratega RRSS experto en PYMEs latinoamericanas.
Empresa: {nombre} — {industria} — {país}
Canales activos: {plataforma, handle, objetivo, frecuencia}
Estrategia actual: {existente o "ninguna"}
Responde SOLO en JSON:
{"objetivo":"...","audiencia":"...","tono":"...","frecuencia":"...",
"cta_principal":"...","embudo":"...","plataformas":[],"pilares":[],"kpis":[]}
Content AI — dos botones, un agente
rrss_content_aiNew La pantalla RRSS › Ideas del panel tiene dos botones separados que disparan el mismo webhook (rrss_content_ai). Lo único que cambia es el campo mode; el agente de n8n debe ramificar según mode (nodo Switch al inicio):
mode: "ideas" → genera ideas nuevas (sin correr el Strategist completo) → POST /api/agent/rrss/ideasmode: "post" → genera el borrador → POST /api/agent/rrss/postsmode: "ideas" con tema → una idea sobre lo que pidió el clientePayload exacto que recibe el webhook (lo arma RrssAgentTriggerController):
{
"company_id": 24,
"trigger": { "source": "ui", "user_id": 1 },
"mode": "ideas", // "ideas" (Generar Ideas) o "post" (Generar Post)
"idea_id": null // ver abajo: ya no llega null en mode "post"
}
mode: "post" ya no manda idea_id: null.
El botón es global, así que ahora manda las ideas que el cliente tiene aprobadas
en idea_ids, y idea_id con la primera de ellas por compatibilidad —
los flujos que ya leen el singular siguen funcionando y dejan de recibir null.
Si no hay ninguna idea aprobada, la plataforma no dispara el agente: devuelve
422 con «Aprueba al menos una idea antes de generar posts.»
{
"company_id": 20,
"trigger": { "source": "ui", "user_id": 1 },
"mode": "post",
"idea_ids": [21, 24, 30], // las aprobadas, en orden de antigüedad
"idea_id": 21 // la primera, por compatibilidad
}
mode: "ideas" acepta un tema del cliente.
El botón «Idea con IA» manda tema (3–300 caracteres), platform y
cantidad: 1. Los tres son opcionales: «Generar Ideas» sigue llegando
sin ellos y el agente debe comportarse como hasta ahora. Cuando llegue tema, la idea
debe ir sobre ese tema, no sobre lo que decida el modelo.
{
"company_id": 20,
"trigger": { "source": "ui", "user_id": 1 },
"mode": "ideas",
"tema": "el error de embalar arte sin esquineras",
"platform": "Instagram",
"cantidad": 1
}
Importante: si el agente ignora mode y siempre genera posts, el botón "Generar Ideas" saldrá mal (era el bug anterior). El branch por mode es obligatorio.
Content AI — modo Ideas
mode: ideasGenera 10 ideas basadas en la estrategia y canales. Llegan con status = idea para revisión del equipo.
company_id, mode = "ideas"GET perfil + canales + estrategiaPOST /api/agent/rrss/ideas — bulk, respuesta: {"ok":true,"created":10}Genera 10 ideas de contenido para {nombre} ({industria}, {país}).
Objetivo: {objetivo} | Pilares: {pilares} | Canales: {plataformas}
Sé específico. Fechas: próximos 30 días.
Responde SOLO en JSON:
{"ideas":[{"topic":"...","platform":"Instagram|Facebook|LinkedIn",
"keyword":"...","fecha_propuesta":"YYYY-MM-DD"}]}
Content AI — modo Post
mode: postToma una idea aprobada y genera el post completo: copy por plataforma, hooks y CTA. Llega como borrador.
company_id, mode = "post", idea_idGET perfil + estrategia + idea específicaPOST /api/agent/rrss/posts — borrador con social_idea_id enlazadoEres copywriter experto en RRSS para PYMEs de LATAM.
Empresa: {nombre} | Tono: {tono} | CTA: {cta_principal}
Tema: {topic} | Plataforma: {platform} | Keyword: {keyword}
Responde SOLO en JSON:
{"copy_instagram":"texto con emojis y hashtags","copy_facebook":"texto conversacional",
"copy_linkedin":"tono profesional","hooks":["hook1","hook2"],
"cta":"llamada a la acción","imagen_prompt":"descripción en inglés para DALL-E"}
Analytics AI
rrss_analyticsrrss_analytics_weekly · NewLee métricas de las plataformas y las carga al backend.
agent_endpoints, cada una con su webhook:
rrss_analytics (mensual) y rrss_analytics_weekly (semanal). Van separadas
y no como un parámetro de la misma fila porque son dos flujos con distinta cadencia:
si compartieran fila, configurar uno apagaría el otro.
POST /api/rrss/run-analytics acepta periodo con
"mensual" (por defecto) o "semanal", y dispara la fila que corresponda.
Sin el campo se comporta exactamente como antes.
curl -s -X POST "https://api.growth54.com/api/rrss/run-analytics" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"periodo":"semanal"}'
El paso de Analytics del pipeline cuenta como configurado con cualquiera de las dos:
un cliente que solo tenga la semanal sí tiene Analytics.
company_idGET /api/n8n/companies/{id}/rrss/canales — handles y plataformasGET /api/n8n/companies/{id}/rrss/posts — para linkear métricas por postPOST /api/agent/rrss/metricasOrden de construcción recomendado
| # | Agente | Por qué primero |
|---|---|---|
| 1 | Content AI — Ideas | Valor inmediato. El cliente ve ideas en segundos. |
| 2 | Content AI — Post | Amplía el anterior. Misma key, mismo workflow. |
| 3 | Strategist AI | Requiere canales bien configurados primero. |
| 4 | Analytics AI (Sheets) | Mínimo viable. Sin OAuth por empresa. |
| 5 | Analytics AI (APIs) | Automatización total. Iteración avanzada. |
- No enviar
company_id/empresa_iden el body → 422 statuscon valor fuera del enum → 422fecha_programadaen el pasado → rechazada- Omitir
Authorizationheader → 401
Telegram — Webhook de entrada
New · 22-julTelegram entrega los mensajes directamente a Growth54, sin pasar por n8n. A diferencia de Meta, no exige revisión de app ni verificar la empresa: basta un bot creado con @BotFather.
Lo llama Telegram, no una persona. Se protege con dos cosas a la vez: el {secret} de la URL,
que identifica a la empresa, y la cabecera X-Telegram-Bot-Api-Secret-Token, comparada en
tiempo constante. Si algo no cuadra responde 404 sin dar pistas.
Siempre responde 200, incluso si algo falla al guardar: si devolviera error, Telegram
reintentaría y acabaría duplicando el mensaje. Los updates que no son texto (fotos, stickers) se
ignoran con {"ok":true,"ignorado":true}.
{
"update_id": 1,
"message": {
"chat": { "id": 555001, "type": "private" },
"from": { "id": 555001, "first_name": "Walter", "username": "waltergz" },
"text": "Hola, quiero información"
}
}
Qué guarda: contacto en wa_contacts con canal = telegram y
external_id = chat_id, conversación en wa_conversations y el mensaje con
rol = user. Las tablas wa_* son ya la bandeja de todos los
canales, no solo de WhatsApp: por eso el CRM enlaza los leads de Telegram sin cambios.
Telegram — Relevo al agente
New · 22-julGrowth54 recibe y guarda siempre; después, si hay un agente configurado, le reenvía el mensaje para que responda. El reparto es: nosotros el transporte y el archivo, el agente el cerebro.
Se registra en Configuración → Agentes con la clave telegram_agent. Growth54
le hace POST a su webhook con este cuerpo:
{
"empresa_id": 23,
"canal": "telegram",
"chat_id": "555001",
"contact_id": 178,
"conversation_id": 116,
"nombre": "Walter Garcia",
"texto": "Hola, quiero información"
}
Para responder, el agente puede usar la API de Telegram con el token del bot, o el endpoint del panel de más abajo. Lo que no debe hacer es registrar su propio webhook en el bot: desconectaría el nuestro.
Telegram — Panel
New · 22-julLectura y gestión desde el panel, siempre acotado a la empresa activa del usuario.
| Método | Ruta | Qué hace |
|---|---|---|
| GET | /api/telegram/stats | Conversaciones, contactos, leads, calientes y negocios en el CRM |
| GET | /api/telegram/conversations | Lista paginada, con el último mensaje de cada una |
| GET | /api/telegram/conversations/{id} | Hilo completo |
| POST | /api/telegram/reply | Responder. Envía directo por la API de Telegram, sin n8n |
| GET | /api/telegram/config | Estado del bot. El token nunca se devuelve: solo si está puesto y sus últimos dígitos |
| PUT | /api/telegram/config | Guardar token y conectar, o {"is_active": false} para desconectar |
| GET | /api/telegram/webhook-info | Diagnóstico: qué webhook tiene Telegram registrado ahora mismo |
wa_reply. Aquí el
envío es directo, y el estado del mensaje (enviado / fallido) refleja lo que
Telegram respondió de verdad — el acuse no miente.
Al guardar el token, el backend lo verifica con getMe contra Telegram antes
de activar nada y registra el webhook solo. Si el token no sirve responde 422 con el motivo
que dio Telegram. El estado de conexión se deriva de esa comprobación real, no se declara.
Correo electrónico — Configuración
New · 22-julDatos de acceso de la cuenta de correo del cliente. Esta fase solo guarda y verifica: la entrada de correos como conversaciones llega después.
| Método | Ruta | Qué hace |
|---|---|---|
| GET | /api/email/config | Cuenta guardada y valores por defecto de Gmail, Outlook y otros |
| PUT | /api/email/config | Guardar. Si secret llega vacío se conserva la contraseña anterior |
| POST | /api/email/config/test | Abre una conexión IMAP real y hace LOGIN para comprobar las credenciales |
Correo electrónico — Envío desde un agente New
AGENT_API_TOKENManda un correo con la cuenta real de la empresa (la que el cliente configuró en Ajustes → Correo electrónico), en vez de una cuenta compartida para todos.
curl -s -X POST "https://api.growth54.com/api/agent/email/send" \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"company_id": 20,
"para": "cliente@ejemplo.com",
"asunto": "Tu reporte de septiembre",
"html": "<h1>Reporte</h1><p>...</p>",
"texto": "Reporte de septiembre"
}'
| Campo | Obligatorio | Notas |
|---|---|---|
company_id | Sí | De qué empresa sale el correo. Determina el SMTP que se usa |
para | Sí | Un correo o una lista: "a@b.com" o ["a@b.com","c@d.com"]. Máximo 50 por llamada New |
asunto | Sí | Máximo 255 caracteres |
html | Sí | Cuerpo del correo |
texto | No | Alternativa en texto plano para lectores que no muestran HTML |
Respuestas:
| Código | Cuerpo | Cuándo |
|---|---|---|
200 | {"ok":true,"message":"Correo enviado.","enviado_desde":"info@empresa.com"} | Enviado |
422 | {"ok":false,"message":"Falta configurar el correo de la empresa…"} | La empresa no tiene SMTP configurado o está inactivo |
422 | {"ok":false,"message":"Destinatario no válido: …"} | Un correo mal formado |
422 | {"ok":false,"message":"Máximo 50 destinatarios por envío…"} | New La lista pasa de 50. Manda el resto en otra llamada |
422 | {"ok":false,"message":"La empresa está inactiva."} | New La empresa está dada de baja |
401 | — | Falta el AGENT_API_TOKEN |
429 | — | New Pasaste de 30 envíos por minuto. Espera y reintenta |
422 y no
500: es un dato que falta, no una caída, y el flujo puede distinguirlo para avisar
al cliente.
WhatsApp Engine — Inbound (agente n8n)
AGENT_API_TOKENEndpoint que el agente n8n llama cada vez que llega un mensaje de WhatsApp. Crea o actualiza el contacto, registra los mensajes y programa follow-ups automáticos. No requiere que exista un número de WhatsApp real — el agente puede llamarlo con datos de cualquier canal mientras se integra Meta API.
curl -s -X POST "https://api.growth54.com/api/agent/whatsapp/inbound" \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"empresa_id": 1,
"phone": "+50588001234",
"name": "Carlos Medina",
"estado": "hot",
"origen": "rrss",
"datos_calificacion": {
"carga": "maquina industrial",
"destino": "Colombia",
"peso_lbs": 300
},
"messages": [
{ "rol": "bot", "contenido": "Hola, ¿en qué te ayudo?", "sent_at": "2026-05-13 10:32:00" },
{ "rol": "user", "contenido": "Quiero cotizar una exportación", "sent_at": "2026-05-13 10:33:00" },
{ "rol": "sys", "contenido": "Lead clasificado como CALIENTE" }
],
"followup": {
"tipo": "24h",
"scheduled_at": "2026-05-14 10:33:00",
"mensaje": "Hola Carlos, ¿sigues interesado en la cotización?"
}
}'
Comportamiento: si el contacto (empresa_id + phone) ya existe, actualiza nombre/estado/origen/calificación. Si la conversación activa ya existe, agrega los mensajes a ella. Si se envía followup, crea un registro en wa_followups.
Campos obligatorios: empresa_id, phone, messages (mínimo 1).
Valores válidos: estado: hot | warm | cold — origen: rrss | web | qr | ai | direct | New facebook | instagram — messages.*.rol: bot | user | sys — followup.tipo: 24h | 72h | 30d | custom.
New 29-jul (B-15): facebook e instagram devolvían 422 aunque el enum de wa_contacts ya los aceptaba desde el 17-jul: faltaba agregarlos a la validación de esta ruta. Ya corregido — se pueden volver a mandar.
New 05-sep: los mensajes siempre se suman a la conversación activa, nunca la reemplazan: no hace falta reenviar el historial completo en cada llamada, basta con los turnos nuevos. Para comprobar que quedaron guardados hay que leer GET /api/whatsapp/conversations/{id} (el hilo), no GET /api/whatsapp/conversations (la bandeja, que devuelve un solo mensaje como vista previa). Desde hoy esta llamada además actualiza el updated_at de la conversación, para que la actividad nueva suba en la bandeja del panel.
Bot — Configuración por empresa
Bearer N8N_TOKENNewNew · 14-ago (#70) La personalidad del bot es una sola para todos los canales: la misma configuración gobierna WhatsApp, Telegram y las respuestas de Community AI en comentarios y DMs de Facebook e Instagram. Hasta ahora solo se podía leer con GET /api/agent/wa/empresa-by-phone/{phone_number_id}, que está indexado por un número de WhatsApp y por eso no le servía a ningún otro canal. Este endpoint entrega lo mismo pedido por empresa.
Devuelve solo comportamiento. Las credenciales de WhatsApp (phone_number_id, wa_access_token) quedan fuera a propósito: son de un canal concreto y no tienen por qué viajar a un flujo de Telegram o de comentarios de Instagram. Si la empresa todavía no configuró su bot, responde 404.
curl -s "https://api.growth54.com/api/n8n/companies/19/bot-config" \
-H "Authorization: Bearer $N8N_TOKEN"
{
"company_id": 19,
"nombre": "Mi Empresa",
"system_prompt": "Eres un agente de ventas de...",
"catalogo": "...",
"faqs": "...",
"msg_bienvenida": "...",
"msg_followup_24h": "...",
"msg_followup_72h": "...",
"palabras_lead_caliente": ["cotizar", "precio"],
"emails_vendedor": ["ventas@empresa.com"],
"modos_respuesta": { // New · 25-ago
"whatsapp": "auto",
"telegram": "auto",
"facebook": "auto",
"instagram": "solo_avisar"
},
"alertar_nueva_conversacion": true // New · 25-ago
}
New 25-ago (#73) — modos_respuesta: mira esto antes de contestar. Hasta hoy el bot respondía solo en todos los canales configurados y el cliente no podía decidir lo contrario. Dos valores por canal: auto (el bot responde solo, el comportamiento de siempre) y solo_avisar (el bot no responde ahí; el mensaje entra igual a la bandeja y se avisa, pero contesta una persona).
Los cuatro canales vienen siempre, aunque la empresa nunca haya tocado la pantalla: lo no configurado sale como auto. Un canal ausente obligaría al flujo a adivinar si eso significa «responde» o «no configurado», que es justo el error que este campo viene a evitar. Los canales son whatsapp, telegram, facebook e instagram (mensajes y comentarios).
⚠️ G54 expone el dato; el corte lo aplica el flujo. Este campo no hace por sí solo que el bot se calle: si el flujo del canal no lo consulta antes de responder, el cliente mueve el interruptor a solo_avisar, la preferencia se guarda… y el bot sigue contestando. Mientras un canal no lea modos_respuesta, ese interruptor no tiene efecto real sobre él.
New 25-ago (#72) — alertar_nueva_conversacion. Booleano: avisar por correo en cuanto entra una conversación nueva, sin esperar a que el lead se marque como caliente. Los destinatarios son los de emails_vendedor; si esa lista está vacía no hay a quién avisar. Arranca en false — prenderlo por defecto llenaría de correo a clientes que nunca lo pidieron.
GET /api/agent/wa/empresa-by-phone/{phone_number_id} no cambia para lo que ya devolvía: los flujos de WhatsApp que ya lo usan siguen igual. New Desde el 25-ago suma los mismos dos campos, más modo_respuesta (singular) ya resuelto al canal de WhatsApp, que es el único que le importa a ese endpoint.
En el panel esta configuración se edita en Ventas › Bot / Asistente. Antes vivía dentro de la pantalla de WhatsApp, lo que hacía creer que solo regía ahí.
Si buscabas GET /api/whatsapp/config: existe, es la ruta del panel y devuelve los mismos dos campos nuevos, pero solo responde con sesión de usuario — un agente no tiene usuario y recibe 401. Desde n8n usa este bot-config, que entrega lo mismo por empresa y con N8N_TOKEN.
WhatsApp Engine — Leads (lectura n8n)
Bearer N8N_TOKENNewEndpoint que el agente consulta al llegar cada mensaje para recuperar la memoria de la conversación. Sin esto, el bot trata cada mensaje como un cliente nuevo y repite preguntas ya hechas. Devuelve el contacto con su historial completo (todos los turnos anteriores) y los datos ya recolectados. Es solo lectura: lo que se escribe sigue yendo por POST /api/agent/whatsapp/inbound.
El phone va url-encoded (ej. %2B17867880417 para +17867880417).
curl -s "https://api.growth54.com/api/n8n/companies/1/wa/leads?phone=%2B17867880417" \
-H "Authorization: Bearer $N8N_TOKEN"
# Respuesta (lead existente):
{
"data": [{
"id": 7,
"contact_name": "Carlos Medina",
"phone": "+17867880417",
"estado": "hot",
"historial": "Agente: Hola, ¿en qué te ayudo?\nCliente: Quiero cotizar",
"datos": { "producto": "cajones cerrados", "medidas": "40x30x20" },
"created_at": "2026-06-20T14:08:00Z",
"updated_at": "2026-06-26T14:08:00Z"
}]
}
# Sin lead para ese teléfono:
{ "data": [] }
Flujo: llega mensaje → GET wa/leads. Si data[0] existe → carga historial y datos (el bot recuerda). Si data[] vacío → cliente nuevo, saluda desde cero. Al responder → POST /api/agent/whatsapp/inbound actualiza historial y datos.
Notas: estado es del contacto (hot | warm | cold); la calificación fina vive en datos (de wa_contacts.datos_calificacion). El historial rotula los turnos como Cliente / Agente / Sistema (mapeo de user / bot / sys).
WhatsApp Engine — Leads (borrado / reset de pruebas)
Bearer AGENT_TOKENNewEndpoints de escritura para que el agente (o quien prueba) limpie leads de WhatsApp sin entrar a la base de datos. Usan el token de agente (AGENT_API_TOKEN), no el N8N_TOKEN de lectura. Al borrar un lead se eliminan en cascada sus conversaciones, mensajes y follow-ups, más el deal del CRM ligado. La empresa va en la URL, así que quedan aislados por empresa.
{"confirm":"DELETE"} en el body como seguro anti-accidentes: evita vaciar la empresa equivocada por un {company} mal escrito.Borra de golpe todos los leads de WhatsApp de la empresa. Ideal para resetear entre pruebas.
curl -s -X DELETE "https://api.growth54.com/api/agent/companies/1/whatsapp/leads" \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm":"DELETE"}'
# Respuesta:
{
"ok": true,
"company_id": 1,
"deleted": { "contacts": 12, "deals": 4 },
"message": "Todos los leads de WhatsApp de la empresa fueron eliminados (contactos, conversaciones, mensajes, followups y deals)."
}
Borra un lead puntual (por id de contacto). No requiere confirm. Devuelve 404 si el contacto no existe o no pertenece a esa empresa.
curl -s -X DELETE "https://api.growth54.com/api/agent/companies/1/whatsapp/leads/7" \
-H "Authorization: Bearer $AGENT_TOKEN"
# Respuesta:
{
"ok": true,
"company_id": 1,
"contact_id": 7,
"deleted_deals": 1,
"message": "Lead de WhatsApp eliminado (contacto, conversaciones, mensajes, followups y deals asociados)."
}
WhatsApp Engine — Panel (Sanctum)
auth:sanctumEndpoints que consume el frontend para mostrar conversaciones, leads y follow-ups. Todos operan sobre la empresa activa del usuario autenticado (current_company_id).
Totales del panel: conversaciones, leads (hot+warm), hot, cotizaciones y ventas_cerradas. Los dos últimos provienen del módulo CRM (deals en etapa proposal+ y closed) — solo lectura, no afectan a los agentes.
Es la bandeja de entrada, no el historial. Lista paginada (30 por página) de conversaciones con los datos del contacto y un solo mensaje: el último, como vista previa. Ordenadas por updated_at descendente, así que la conversación con actividad más reciente va primero.
New 05-sep: que messages traiga un elemento es deliberado y no significa que la conversación tenga un solo mensaje. Para leer el hilo entero hay que ir a GET /api/whatsapp/conversations/{id}. Se documenta porque se prestaba a leerlo como pérdida de datos: la lista devolvía 200 con un mensaje y parecía que los demás no se habían guardado.
Corregido el 05-sep: esa vista previa devolvía el mensaje más viejo en vez del último (el orden de la relación pisaba al de la consulta), y una conversación con mensajes nuevos no subía en la lista porque inbound no tocaba el updated_at de la conversación. Las dos cosas están arregladas y cubiertas por tests.
El hilo completo. Datos del contacto + todos los mensajes, ordenados por sent_at ascendente (y por id cuando coinciden en el mismo segundo, que es lo normal cuando el bot contesta al instante). Es el endpoint que hay que usar para verificar que un mensaje se guardó, no la lista.
Follow-ups activos (estado: pendiente | programado | activo) ordenados por scheduled_at. Incluye nombre y teléfono del contacto.
curl -s -X POST "https://api.growth54.com/api/whatsapp/reply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": 12,
"message": "Hola, te confirmo la cotización."
}'
Respuesta manual del operador desde el panel. Guarda el mensaje saliente (rol: bot) en la conversación de la empresa activa y lo reenvía a n8n como transporte hacia WhatsApp (Graph API). La conversación debe pertenecer a la empresa activa.
Devuelve New: siempre 201 cuando el mensaje se guarda, con { ok, sent, data, message }. El mensaje guardado incluye ahora estado (enviado | fallido | pendiente) y transporte_detalle (código HTTP y respuesta de n8n, o el error de red). ok y sent valen false si el transporte no aceptó el envío. Antes, si el webhook no estaba configurado se devolvía 422; ahora el mensaje se guarda y se marca fallido.
⚠️ Alcance de sent: significa que n8n aceptó el POST, no que Meta haya entregado el mensaje al cliente. Los acuses reales de Meta (sent → delivered → read) aún no se reciben.
curl -s -X DELETE "https://api.growth54.com/api/whatsapp/leads/45" \
-H "Authorization: Bearer $TOKEN"
Reset total de un lead de WhatsApp (herramienta de debugging para limpiar pruebas). El {id} es el contact.id que devuelve GET /api/whatsapp/conversations. Borra el contacto y todo lo que cuelga de él: conversaciones, mensajes, follow-ups y su(s) ficha(s) del CRM (con sus actividades y eventos). No hay UI: es solo API a propósito, por ser destructivo.
Aislado por empresa activa: solo borra leads de la empresa del token; un id de otra empresa devuelve 404. Respuesta 200: { ok, contact_id, deleted_deals, message }.
POST https://n8n.mdarthurdigital.com/webhook/wa-reply-g54
Content-Type: application/json
Authorization: Bearer $N8N_WA_REPLY_WEBHOOK_TOKEN # solo si el env está definido
{
"phone": "+5215512345678",
"message": "Hola, te confirmo la cotización.",
"company_id": "20",
"phone_number_id": "1083260611538246"
}
Contrato de salida. Growth54 envía exactamente esos cuatro campos. El destino se configura en agent_endpoints (key wa_reply) o en el env N8N_WA_REPLY_WEBHOOK_URL. Timeout: 12 s (6 s de conexión). Cualquier respuesta 2xx se interpreta como aceptada.
New phone_number_id: el número de Meta de la empresa activa, tomado de wa_configs. Sirve para resolver las credenciales de forma dinámica, sin cablearlas por empresa: pasarlo a GET /api/agent/wa/empresa-by-phone/{phone_number_id}, que devuelve company_id, wa_access_token y el resto de la config.
Si la empresa no tiene WhatsApp configurado, Growth54 no llama al webhook y marca el mensaje como fallido.
⚠️ n8n (lado Solange) — cuatro condiciones:
- Solo transporte. Enviar a WhatsApp por Graph API, pero NO volver a guardar el mensaje en G54: el panel ya lo persiste y se duplicaría. (La API solo acepta
rolbot,userosys;agent/agentedevuelve 422.) - Nombres de campos. El flujo debe leer
phone,message,company_idyphone_number_idtal cual (notelefono,texto,mensajenito). - Credenciales dinámicas. Resolver el token con
empresa-by-phoneusando elphone_number_idrecibido. No cablear tokens por empresa: no escala multiempresa. - Autenticación. Si el webhook exige token, avisar para definir
N8N_WA_REPLY_WEBHOOK_TOKEN; hoy no se envía cabeceraAuthorization.
Responder 2xx solo si el envío a Meta salió bien. Si n8n responde 200 antes de llamar a Graph API, el panel marcará enviado un mensaje que nunca llegó.
Casos de uso
GET /api/n8n/companies→ obtenerempresa_idPOST /api/agent/keywords/bulk→ insertar keywords
GET /api/n8n/companies/{id}/keywords/pending-articulos- El agente genera HTML para cada keyword
POST /api/agent/articulos
- Panel llama
POST /api/rrss/run-content-aiconmode: ideas - n8n lee estrategia y canales → IA genera ideas →
POST /api/agent/rrss/ideas - El equipo aprueba desde el panel →
PUT /api/rrss/ideas/{id}/status
Planes de Suscripción
Documentación completa de los planes disponibles en Growth54: funcionalidades incluidas, límites por plan y comparativa. Relevante para el equipo comercial y para entender qué módulos están disponibles para cada cliente.
💳 Abrir documento de planes →Checklist rápido
- Insertar datos desde n8n:
/api/n8n/companies→ id →/api/agent/* - Acceso de usuario: login → switch-company → usar módulos
- Activar protección: definir
N8N_API_TOKENyAGENT_API_TOKENen.env - Siempre enviar:
Accept: application/jsonyContent-Type: application/json
POST /api/dev/purge-current-company elimina todos los datos de la empresa activa. Nunca en producción.