Skip to content
github-actions[bot] edited this page Oct 14, 2026 · 4 revisions

API del backend

Resumen. El backend de KopTup es una API REST en Express (Node.js) que guarda los datos en MongoDB, usa Redis para sesiones, cupos y topes de gasto, y llama a OpenAI en las demos con IA. Todas las rutas viven bajo /api/… (salvo las de salud) y cada una aplica su política de autorización en el servidor: pública, con sesión, dueño del recurso, equipo, administrador o "acceso a la demo". Esta página es la referencia completa, módulo por módulo, con los errores, los límites de uso y los flujos clave dibujados.

Para quién: un desarrollador que va a usar o cambiar la API, y el dueño que quiere entender qué pasa "por dentro" cuando alguien pide una demo, la aprueba el equipo y la abre el prospecto. Todos los ejemplos son respuestas reales del backend local con datos de ejemplo; los tokens se muestran como <oculto>.

En esta página:

  1. La API en una imagen
  2. Convenciones
  3. Quién puede llamar cada ruta
  4. Referencia por módulo
  5. Códigos de error comunes
  6. Límites de uso y topes de gasto
  7. Flujos clave paso a paso
  8. Ejemplos con curl
  9. Salud del servicio y documentación interactiva
  10. Para desarrolladores
  11. Limitaciones conocidas
  12. Páginas relacionadas

1. La API en una imagen

Cada petición pasa por las mismas capas antes de llegar al código que hace el trabajo. Si una capa la rechaza, la petición no sigue.

flowchart TD
    classDef cliente fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef capa fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef dato fill:#f1f5f9,stroke:#64748b,color:#334155
    classDef no fill:#fee2e2,stroke:#dc2626,color:#7f1d1d

    C["Web koptup.com, curl o servidor de la web"]:::cliente
    H["Cabeceras de seguridad (helmet)"]:::capa
    CO{"Origen permitido (CORS)?"}
    J["Lee el cuerpo JSON (máx. 2 MB)"]:::capa
    RL{"Dentro del cupo?"}
    R["Enrutador por prefijo /api/..."]:::capa
    P{"Política de la ruta"}
    K["Controlador o servicio"]:::capa
    M[("MongoDB")]:::dato
    RE[("Redis")]:::dato
    AI["OpenAI y correo"]:::dato
    E403["403 cors_forbidden"]:::no
    E429["429 demasiadas peticiones"]:::no
    E4xx["401, 403 o 503"]:::no

    C --> H --> CO
    CO -- no --> E403
    CO -- "sí" --> J --> RL
    RL -- no --> E429
    RL -- "sí" --> R --> P
    P -- rechaza --> E4xx
    P -- permite --> K
    K --> M
    K --> RE
    K --> AI
Loading

De arriba abajo: seguridad, CORS, tamaño del cuerpo, cupo de peticiones, política de la ruta y, al final, el controlador que lee o escribe en MongoDB, Redis u OpenAI.

El recorrido más importante de la API es el de una demo: alguien la pide, el equipo la aprueba, la persona activa su cuenta y la abre. El GIF muestra las peticiones reales de ese recorrido, una por cuadro:

Recorrido de la API desde la solicitud hasta Mis demos

GIF: ocho peticiones reales (solicitud, panel, aprobación, doble clic, revisión del enlace, activación, acceso a la demo y Mis demos) con las respuestas del backend local.

Cuadro Petición Quién la hace Qué demuestra
1 POST /api/demo-requests Visitante (sin sesión) Se guarda la solicitud con código DR-AAAA-XXXXXX y estado pendiente
2 GET /api/admin/demo-requests Equipo El panel lista solicitudes con conteos por estado
3 POST /api/admin/demo-requests/:id/approve Admin o sales Crea la cuenta prospect (invitado), un acceso por demo y el enlace de activación
4 El mismo approve otra vez Admin o sales Un doble clic responde 409 already_processed y no duplica nada
5 POST /api/auth/activate/check y POST /api/auth/login Prospecto El enlace se revisa sin gastarlo; antes de activar, el login responde account_not_activated
6 POST /api/auth/activate Prospecto Fija la contraseña y devuelve la sesión; el enlace ya no sirve una segunda vez
7 GET /api/demo-access/erp Servidor de la web Sin sesión: sin_sesion; con la sesión de Laura: allowed: true, motivo grant
8 GET /api/me/demos Prospecto Sus accesos con días restantes y visitas

2. Convenciones

Tema Regla
URL base La web la toma de su variable NEXT_PUBLIC_API_URL (ver Operación y despliegue). En desarrollo el backend escucha en http://localhost:3001 (PORT). En esta página usamos $API para no repetirla
Formato JSON en la petición y en la respuesta (Content-Type: application/json), salvo las subidas de archivos (multipart/form-data) y las descargas de Excel
Respuesta correcta { "success": true, "data": …, "message"?: "…" }. Las listas paginadas traen data.items, data.total, data.page, data.pages y, en el panel de demos, data.conteos
Respuesta de error { "success": false, "code": "…", "message": "…" } y, según el caso, fields, errores, motivo o retryAfterSec (ver sección 5). Todas las respuestas 4xx y 5xx traen code: las rutas antiguas que responden a mano pasan por un middleware que completa code (según el estado HTTP) y message
Sesión Cabecera Authorization: Bearer <access token>. El access token es un JWT que dura 15 minutos (JWT_EXPIRES_IN); el refresh token dura 7 días (JWT_REFRESH_EXPIRES_IN) y se guarda en Redis, uno por cuenta
Rol Va dentro del token, pero las rutas del equipo y las de demos vuelven a leer el rol en MongoDB en cada petición: si a alguien le quitan el rol, pierde el acceso de inmediato
Fechas ISO 8601 en UTC (2026-10-23T07:56:09.487Z). La web las muestra en hora de Colombia
Identificadores Casi todo usa el _id de MongoDB (24 caracteres hexadecimales). Excepciones: pedidos (ORD-0001), entregables (DEL-0001), conversaciones (CONV-0001), solicitudes de demo (código DR-2026-XXXXXX, además del _id) y bots del chatbot (kbot_…)
Paginación ?page=1&limit=20 (máximo 100) en los listados del panel de demos y la bitácora
CORS Solo los orígenes https://koptup.com y https://www.koptup.com (más localhost:3000 y 3001 fuera de producción y los que agregue CORS_ORIGIN). Las peticiones sin cabecera Origin (curl, servidor a servidor) no pasan por CORS
Cabeceras propias X-Bot-Owner-Token (dueño de un bot del chatbot). El servidor de la web usa además una cabecera interna para informar la IP real del visitante
Tamaño del cuerpo 2 MB para JSON y formularios; 15 MB solo en POST /api/chatbot/bots/:botId/docs (archivos en base64)

3. Quién puede llamar cada ruta

Cada ruta declara una de estas políticas. El inventario completo vive en el código, en apps/backend/src/routes/POLICIES.md; aquí está resumido y en la sección 4 aparece ruta por ruta. El detalle de los roles está en Roles y permisos.

Política Quién pasa Si no pasa
Pública Cualquiera (con cupo de peticiones y, si usa IA, tope de gasto) 429 o 503
Autenticado Un access token válido 401 No token provided, Invalid token o TOKEN_EXPIRED
Dueño del recurso Sesión + el controlador comprueba que el registro es de esa cuenta (o que es del equipo) 403 o 404
Equipo Roles admin, manager, sales 403 Forbidden - Insufficient permissions
Admin Rol admin 403
Acceso a demo Equipo (admin, manager, sales, developer) o quien tenga un acceso vigente a esa demo; las demos en modo publico quedan abiertas 401 login_required, 403 demo_access_required o demo_disabled, 503 si MongoDB no responde
Dueño del bot Quien presente el token del bot en X-Bot-Owner-Token 401 owner_token_required o 403 forbidden_not_owner
Solo desarrollo Admin, y solo fuera de producción 404 en producción
Webhook firmado Meta, con la cabecera X-Hub-Signature-256 (HMAC-SHA256 del cuerpo crudo con la clave secreta de la app) 401 invalid_signature; 404 si el webhook no está configurado

La política Acceso a demo es la que conecta la API con el sistema de demos. Así decide el servidor:

flowchart TD
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef no fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef dec fill:#fef3c7,stroke:#d97706,color:#78350f

    A["Petición a una API de demo"] --> B{"Token válido y rol de equipo?"}:::dec
    B -- "sí" --> OK1["Pasa (motivo staff)"]:::ok
    B -- no --> C{"La demo existe y está activa?"}:::dec
    C -- no --> N1["403 demo_disabled"]:::no
    C -- "sí" --> D{"Modo de la demo"}:::dec
    D -- publico --> OK2["Pasa (motivo publico)"]:::ok
    D -- "solicitud o privado" --> E{"Hay sesión?"}:::dec
    E -- no --> N2["401 login_required, motivo sin_sesion"]:::no
    E -- "sí" --> F{"DemoGrant activo con expiresAt futuro?"}:::dec
    F -- "sí" --> OK3["Pasa (motivo grant) y registra el uso"]:::ok
    F -- no --> N3["403 demo_access_required, motivo expirado, revocado o sin_acceso"]:::no
Loading

La vigencia se compara con la hora actual en cada petición: un acceso vencido o revocado se niega al instante, sin esperar el proceso que corre cada hora.

La ruta pública GET /api/demo-access/:slug usa exactamente la misma regla, pero siempre responde 200 y pone la decisión en data. Así la ve la web al abrir /demo/<slug>:

Cuatro respuestas reales de GET /api/demo-access

Demo pública sin sesión (publico), demo con solicitud sin sesión (sin_sesion), con un acceso vigente (grant, con días restantes) y demo privada sin acceso (sin_acceso).

Lo que la web muestra en cada caso está en Flujos del prospecto y cliente.


4. Referencia por módulo

Convenciones de las tablas: Auth es la política de la sección 3; :id es el _id de MongoDB salvo que se diga otra cosa; "→" separa lo que se envía de lo que se recibe.

4.1 Salud y documentación

Método Ruta Auth Para qué Entrada → salida
GET /health/live Pública El proceso responde (aunque no haya base de datos) → 200 { status: "alive", timestamp }
GET /health Pública Listo para atender (MongoDB conectado). Healthcheck de Railway y del monitoreo → 200 { status: "healthy", mongo: "connected", timestamp, uptime } o 503 { status: "unavailable", mongo: "disconnected" }
GET /api-docs Solo fuera de producción o con API_DOCS_ENABLED=true Swagger UI Página HTML

4.2 Autenticación (/api/auth)

Método Ruta Auth Para qué Entrada → salida
POST /register Pública · 5 por hora por IP Crear una cuenta local (rol user) desde /register { email, password (mín. 8), name } → 201 { id, email, name }; 400 User already exists
POST /login Pública · 10 cada 15 min por IP y correo, 30 por IP Iniciar sesión { email, password } → { accessToken, refreshToken, user: { id, email, name, role } }; 401 Invalid credentials o account_not_activated
POST /refresh Pública (exige un refresh token válido) Pedir un access token nuevo { refreshToken } → { accessToken }; 401 Invalid refresh token
POST /logout Autenticado Cerrar sesión (borra el refresh token de Redis) → mensaje
GET /me y /profile Autenticado Datos vigentes de la cuenta (la web protege /admin y /dashboard con esta ruta) → { id, email, name, role, phone, company, provider, accountStatus, created_at, last_login }
PATCH /me Autenticado Editar nombre, teléfono y empresa propios { name?, phone?, company? } (otros campos se rechazan) → la cuenta; 400 invalid_profile
POST /change-password Autenticado · 5 cada 15 min por cuenta Cambiar la contraseña conociendo la actual { currentPassword, newPassword } → sesión nueva; 400 wrong_password, invalid_password o no_local_password
POST /forgot-password Pública · 3 cada 15 min por IP y correo, 10 por IP Enviar el enlace de recuperación (1 hora) { email } → siempre el mismo mensaje, exista o no la cuenta
POST /reset-password Pública · 10 cada 15 min por IP Fijar una contraseña nueva con el enlace { token, password } → mensaje; 400 token_invalid, token_used, token_replaced o token_expired
POST /activate/check Pública · 20 por minuto Revisar el enlace de activación sin gastarlo { token } → { nombre, emailEnmascarado, expiresAt }
POST /activate Pública · 10 cada 15 min por IP Activar la cuenta invitada y fijar la contraseña { token, password } → misma respuesta que el login; 400 token_* o invalid_password
GET /google y /google/callback Pública Entrar con Google Redirige a Google y luego a /auth/callback de la web con un código de un solo uso (60 s); 503 si Google no está configurado
POST /google/exchange Pública · 20 por minuto La web canjea ese código por la sesión { code } → misma respuesta que el login; 400 token_used, token_expired o token_invalid

4.3 Contacto y leads

Método Ruta Auth Para qué Entrada → salida
POST /api/contact Pública · 5 por minuto Formulario de /contact: guarda un Contact (origen contact-form) con la prueba de la autorización (consentimiento: fecha, versión de la política del servidor, hash de la IP y navegador) y avisa al dueño por correo y por WhatsApp sin esperar a los proveedores (el resultado por canal queda en Contact.notificaciones; ver 4.15). Si el campo trampa website llega lleno responde 200 sin guardar nada { name, email, service, message, consentimiento: true, website?, phone?, company?, budget?, timeline?, quote? } → mensaje. Sin consentimiento: true → 400 consent_required (fields: ["consentimiento"]). Datos inválidos → 400 invalid_request con fields y errores ({ campo: mensaje }) en español; límites: nombre 120, empresa 160, teléfono 40, servicio 300 y mensaje 5.000 caracteres. timeline: now, 1m, 3m u open (se guarda como plazo). quote: { service, plan?, tier?, modality? } con la cotización que el visitante venía revisando (se guarda como cotizacion): service es sistema-rag, una solución del catálogo o un servicio que el portal enlaza; plan solo va con sistema-rag, y tier y modality solo con una solución del catálogo. Un valor desconocido o incoherente responde 400
GET /api/admin/contacts admin, manager Bandeja de contactos del panel ?status=new|read|responded → lista con source (contact-form, demo-rag o demo-request), plazo, cotizacion y notificaciones ({ email, whatsapp } con estado, intentos, ultimoIntento, proximoIntento, error y respaldo; null en los contactos anteriores al registro de avisos)
PATCH /api/admin/contacts/:id/status admin, manager Marcar leído o respondido { status } → { id, status }
POST /api/admin/contacts/:id/notify admin · 10 cada 15 min por cuenta Reenviar el aviso al dueño de ese contacto: los canales que no salieron (fallido, pendiente vencido o no configurado) o, con ?todos=1, los dos. Queda en la bitácora (contact.notify_resend) → { id, notificaciones, resultado: { email, whatsapp } } (cada uno con enviado, estado, omitido, error y, en WhatsApp, respaldo); 404 not_found; 409 contact_anonymized; 429 rate_limited

Las rutas de prueba POST /api/contact/test-whatsapp y /api/contact/test-email se retiraron (solo existían fuera de producción y responden 404): la prueba de los avisos es POST /api/admin/notifications/test (4.15).

4.4 Demos: lo público y el portal

Método Ruta Auth Para qué Entrada → salida
POST /api/demo-requests Pública: campo trampa website, autorización de datos obligatoria, 5 envíos válidos por hora por IP y 3 por día por email Formulario "Solicitar demo". Si el mismo email tiene una solicitud abierta de los últimos 30 días, le suma las demos en vez de crear otra { nombre, empresa, cargo?, email, telefono?, pais, tamanoEmpresa, demos[1–10], casoDeUso (10–2000), consentimiento: true, versionPolitica?, origen? } → 201 { codigo, estado, fusionada }
GET /api/demo-catalog Pública (caché de 30 s) Catálogo de las 28 demos → [{ slug, nombre, nombreEn, accessMode, activo }] y fuente (bd o respaldo si MongoDB falla)
GET /api/demo-access/:slug Pública con sesión opcional · 120 por minuto ¿Esta sesión puede abrir la demo? Con un acceso vigente cuenta una visita (una nueva tras 30 minutos sin actividad) ?registrar=0 (no cuenta la visita) → { slug, allowed, accessMode, activo, reason, expiresAt, diasRestantes }; 404 not_found; 401 TOKEN_EXPIRED
GET /api/me/demos Autenticado Mis demos: los accesos de la propia cuenta → [{ id, demoSlug, demoNombre, accessMode, estado, vigente, expiresAt, diasRestantes, ultimoAcceso, accesos, url }]

Los valores de reason son: staff, publico, grant, sin_sesion, sin_acceso, expirado, revocado, desactivada, no_existe y no_disponible.

4.5 Demos: panel del equipo (/api/admin/demo-…)

Estas rutas se montan antes de /api/admin para que el rol sales pueda gestionar solicitudes y accesos sin entrar al resto del panel.

Método Ruta Auth Para qué Entrada → salida
GET /api/admin/demo-requests admin, sales, manager Listar solicitudes ?estado=&demo=&q=&page=&limit= → { items, total, page, pages, conteos }
GET /api/admin/demo-requests/:id admin, sales, manager Detalle con la cuenta ligada, los accesos y el historial (bitácora) → { request, demos, cuenta, grants, historial }
PATCH /api/admin/demo-requests/:id admin, sales Pasar a en_revision o volver a pendiente, o agregar una nota { estado?, nota? } → la solicitud; 409 invalid_transition
POST /api/admin/demo-requests/:id/approve admin, sales; con demos privadas, solo admin Aprobar: crea o vincula la cuenta, un acceso por demo y, si hace falta, el enlace de activación { demos?, dias? (1–365), nota?, mensaje? } → { request, user, grants, activationUrl, activationExpiresAt, loginUrl, email }; 409 already_processed; 403 requires_admin; 422 team_email
POST /api/admin/demo-requests/:id/reject admin, sales Rechazar con motivo interno (y, si se pide, avisar con un mensaje aparte) { motivo (mín. 3), notificar?, mensaje? } → { request, email, notificado }
POST /api/admin/demo-requests/:id/resend-activation admin, sales Emitir un enlace de activación nuevo (invalida el anterior) → { user, activationUrl, activationExpiresAt, email }; 409 not_approved o account_active
GET /api/admin/demo-grants admin, sales, manager Listar accesos ?estado=activo|por_vencer|expirado|revocado&demo=&q=&user=&request=&page=&limit= → { items, total, page, pages, conteos }
POST /api/admin/demo-grants admin, sales; con demos privadas, solo admin Invitación directa, sin solicitud previa { email, nombre?, empresa?, telefono?, demos[1–10], dias?, nota?, mensaje? } → 201, misma forma que approve
POST /api/admin/demo-grants/:id/extend admin, sales (privadas: admin) Sumar días (también reactiva un acceso vencido) { dias (1–365) } → { grant, email }; 409 grant_revoked o active_grant_exists
POST /api/admin/demo-grants/:id/revoke admin, sales (privadas: admin) Cerrar el acceso { motivo? } → { grant }; 409 grant_revoked
POST /api/admin/demo-grants/:id/resend-activation admin, sales Enlace nuevo para la cuenta del acceso (no da accesos) → igual que en solicitudes
GET /api/admin/demo-catalog admin, sales, manager Catálogo con vigencia por defecto y orden → [{ slug, nombre, nombreEn, accessMode, activo, duracionDiasPorDefecto, orden, updatedAt }]
PATCH /api/admin/demo-catalog/:slug (o PATCH / con slug) admin Cambiar modo, activa, vigencia, orden o nombres { accessMode?, activo?, duracionDiasPorDefecto?, orden?, nombre?, nombreEn? } → la entrada; 422 fixed_mode si se intenta cerrar el chatbot RAG
GET /api/admin/audit-log admin Bitácora de acciones del equipo (no tiene pantalla en el panel: se consulta por API o en el historial de cada solicitud) ?accion=&entidadTipo=&entidadId=&page=&limit= → { items: [{ accion, actorEmail, actorRol, entidad, detalle, fecha }], total, page, pages }
POST /api/admin/privacy/search admin Derechos del titular (Ley 1581): todo lo que el sistema guarda de una persona. El email va en el cuerpo (no en la URL) { email } → { correosBuscados, resumen: { categoría: n }, bloqueos, datos, plazos }. Email inválido → 400 con fields: ["email"]
POST /api/admin/privacy/export admin Lo mismo como archivo para entregar al titular { email } → JSON adjunto datos-titular-AAAA-MM-DD.json, sin contraseñas ni hashes de enlaces
POST /api/admin/privacy/erase admin Supresión irreversible { email, confirmacion } (el mismo email) → { referencia: "SUP-AAAA-XXXXXXXX", eliminados, anonimizados }. 400 confirmation_mismatch; 404 not_found si no hay datos; 409 team_account o 409 contractual_records (con bloqueos) si no procede. Las tres rutas dejan en la bitácora privacy.search, privacy.export o privacy.erase sin datos personales

Las pantallas que usan estas rutas están en el Manual del administrador; por ejemplo, el enlace de activación que devuelve approve es el que el panel muestra para copiar o enviar por WhatsApp.

4.6 Administración (/api/admin)

Todo este router exige admin o manager, salvo el cambio de rol (solo admin).

Método Ruta Para qué Entrada → salida
GET /orders Todos los pedidos ?status= → lista con cliente, ítems, adjuntos y estado de aprobación
PATCH /orders/:orderId/status Cambiar el estado de un pedido (solo el equipo cambia estados) { status: pending|in_progress|shipped|completed|cancelled }; 400 con otro valor
POST /orders/:orderId/approve Aprobar un pedido → { id, approvalStatus: "approved" }
POST /orders/:orderId/reject Rechazar (también lo cancela) { reason? }
POST /orders/:orderId/invoice Crear la factura de un pedido (ver Limitaciones) → 201 { id, invoiceNumber }
GET /invoices Todas las facturas ?status=
GET /deliverables Todos los entregables ?status=&projectId=
GET /conversations y /conversations/:id Conversaciones y su detalle ?status=
GET /users Usuarios ?role=&search= → { id, name, email, role, provider, lastLogin, createdAt }
PATCH /users/:id/role Cambiar el rol (solo admin; nadie puede quitarse su propio rol de admin) { role } → { id, email, role }; queda en la bitácora
GET /contacts, PATCH /contacts/:id/status y POST /contacts/:id/notify (solo admin) Ver 4.3

En estas rutas :orderId es el código del pedido (ORD-0001), no el _id.

4.7 Portal del cliente: proyectos, pedidos, facturas, entregables, mensajes y notificaciones

Todas exigen sesión. Cada controlador filtra por la cuenta que llama (o permite al equipo, donde se indica).

Método Ruta Auth Para qué Entrada → salida
GET /api/projects/dashboard/stats Autenticado Resumen del portal → { totalProjects, activeProjects, tasks, recentActivity }
GET /api/projects Autenticado Proyectos donde la cuenta es cliente o gerente → lista
POST /api/projects Autenticado Crear un proyecto (si no se indica manager_id, quien lo crea queda como gerente) { name, description?, client_id?, manager_id?, status?, priority?, budget?, start_date?, end_date?, estimated_hours? }
GET / PUT / DELETE /api/projects/:id Cliente o gerente (borrar: solo el gerente) Ver, editar o borrar PUT acepta name, description, status, priority, budget, fechas, horas, progress
GET /api/projects/:id/members, /api/projects/:id/tasks Participante Miembros y tareas → listas
POST /api/projects/:id/tasks, PUT /api/projects/tasks/:id Participante Crear o editar tareas { title, description?, status?, priority?, assigned_to?, due_date? … }
GET /api/orders Dueño Mis pedidos ?status=
POST /api/orders Autenticado Crear un pedido con adjuntos (hasta 10 archivos de 20 MB) multipart: name, description?, items (JSON [{ name, quantity, price }]), projectId?, comments?, attachments[] → 201 { id: "ORD-…", amount, status }
GET /api/orders/:orderId Dueño Ver el pedido → incluye approvalStatus, adjuntos y editable (si el cliente aún puede cambiarlo)
GET /api/orders/:orderId/attachments/:attachmentId Dueño del pedido o equipo (admin, manager, sales) Descargar un adjunto (lo usa Admin › Pedidos) → el archivo como descarga; 404 not_found si el adjunto no existe; 410 file_unavailable si el archivo ya no está en el servidor (sin volumen, Railway lo borra al redesplegar)
PUT /api/orders/:orderId Dueño, solo mientras el pedido está pendiente de revisión Editar nombre, descripción y adjuntos (el estado lo cambia el equipo) JSON o multipart: name?, description?, attachments[] (agrega), removeAttachments (ids a quitar) → el pedido; 403 status_team_only, 400 invalid_fields, 409 order_not_pending
POST /api/orders/:orderId/cancel Dueño Cancelar, solo mientras el pedido está pendiente de revisión 409 order_not_pending si el equipo ya lo revisó o está en curso
GET /api/invoices Dueño Mis facturas ?status=&startDate=&endDate=
POST /api/invoices admin, manager Emitir una factura { projectId, clientInfo, items, taxRate?, dueDate?, notes?, terms? }
GET /api/invoices/:id Dueño o equipo Detalle —
POST /api/invoices/:id/pay admin, manager El equipo registra el pago que confirmó (transferencia, enlace); el cliente no marca sus facturas como pagadas { paymentMethod, transactionId? }
GET /api/invoices/payment-methods Autenticado Métodos de pago que el equipo configuró en Admin › Configuración (hoy, las instrucciones de transferencia). El portal muestra "Cómo pagar" solo si hay alguno → { metodos: [{ tipo: "transferencia", instrucciones }] } o []
GET /api/invoices/:id/download Dueño o equipo Devuelve datos para descargar → { downloadUrl, fileName, expiresIn }
GET /api/deliverables, /api/deliverables/:id Dueño (detalle: dueño o equipo) Mis entregables ?status=&projectId=
POST /api/deliverables admin, manager, developer Registrar un entregable { projectId, title, type, fileUrl, fileName, fileSize, description?, fileType?, metadata? }
PUT /api/deliverables/:id Dueño Cambiar título o descripción —
POST /api/deliverables/:id/approve, /reject Dueño o equipo Aprobar o rechazar (rechazar exige comments) { comments? }
GET / POST /api/messages/conversations Participante (crear: equipo admin, manager o sales) Mis conversaciones o crear una POST { title, clientId, projectId?, orderId? }
GET /api/messages/conversations/:id Participante Conversación con sus mensajes :id es CONV-…
POST /api/messages/conversations/:id/read Participante Marcar como leída —
POST /api/messages/send Participante Enviar un mensaje { conversationId, content, type?, attachment? }
GET /api/notifications, /api/notifications/unread-count Dueño Mis notificaciones ?type=&unreadOnly=&limit=
POST /api/notifications Autenticado (para otra cuenta: equipo) Crear una notificación { type, title, message, actionUrl?, metadata?, targetUserId? }
POST /api/notifications/:id/read, /read-all; DELETE /api/notifications/:id Dueño Marcar o borrar —

4.8 Chatbot RAG (/api/chatbot)

Toda la ruta tiene su propio cupo: 180 peticiones por minuto por IP (GET /models no cuenta) y las preguntas al chat, otro de 60 por minuto por IP. Los bots, sus documentos y el historial se guardan en MongoDB (ver Modelos de datos); sin base de datos, las rutas /bots… responden 503 storage_unavailable.

Método Ruta Auth Para qué Entrada → salida
GET /models Pública (no cuenta en el cupo por IP) Modelos permitidos, si hay IA y si existe POST /sample-bots → { available: [{ id, name, costInputUSDper1M, costOutputUSDper1M, enabled }], activeProvider, sharedSampleBots: true }
POST /bots Pública · 30 bots por hora por IP; máx. 20.000 bots (CHATBOT_MAX_BOTS) Crear un bot. kind: "sample" marca un bot de ejemplo de una demo: nunca guarda conversaciones y se borra a los 30 días sin uso { name?, color?, position?, avatar?, welcome?, systemPrompt?, tone?, languages?, kind? } → 201 con el bot (incluye kind) y ownerToken (solo esta vez)
GET /bots Pública Bots del dueño (según X-Bot-Owner-Token) y bots de ejemplo ?limit=&offset= → lista resumida
GET /bots/:botId Pública Configuración (completa solo para el dueño; sin prompt ni documentos para el resto) —
PATCH / DELETE /bots/:botId Dueño del bot (un bot antiguo sin dueño registrado: solo el equipo) Editar o borrar el bot con sus documentos y conversaciones —
POST /bots/:botId/docs Dueño del bot Subir archivos en base64 (máx. 5 por envío, 5 MB c/u, 50 por bot) { files: [{ name, mime, contentBase64 }] } → { docs, added, errors }
DELETE /bots/:botId/docs/:docId Dueño del bot Quitar un documento —
POST /bots/:botId/urls Dueño del bot Indexar páginas web (máx. 5 por envío, 2 MB c/u) { urls: [...] }
POST /bots/:botId/chat Pública (bot existente) · 60 preguntas por minuto por IP Preguntar. Sin IA, sin presupuesto o con error del proveedor responde en modo extractivo citando fragmentos, con una nota que lo explica y error = llm_not_configured, budget_exhausted, budget_unavailable o llm_provider_error. Los textos que escribe el servidor salen en lang (es o en; si no viene, el primer idioma del bot). Solo con channel: "widget" (lo envía /embed/chatbot fuera de la vista previa del Builder) y en un bot de usuario se guarda la pregunta en el historial, que vence a los 30 días { message (≤ 2000), history?, model?, channel?, lang? } → { reply, sources, confidence, model, tokens?, costUSD?, error? }
GET / DELETE /bots/:botId/conversations Dueño del bot Historial (últimos 200 turnos vigentes) o borrarlo ?limit=
POST /sample-bots Pública (cuenta en el límite de creación solo la primera vez) Bot de ejemplo compartido de «Prueba el asistente»: el servidor lo identifica por la huella de su configuración y sus textos, lo crea e indexa una sola vez y lo reutiliza; no tiene dueño, no guarda conversaciones y se borra a los 30 días sin uso { sampleKey, config, files: [{ name, mime (TXT/MD/CSV), contentBase64 (≤ 256 KB) }] (1 a 5) } → { botId, docs, docsCount, shared: true }

La API anterior "por sesión" (/session, /upload, /message, /info, /config, /messages, /documents) se retiró: responde 404 y, al arrancar, el backend borra lo que dejó (colección chatbots y uploads/chatbot).

4.9 Demo "Prueba con tu documento" (/api/demo-rag)

Método Ruta Auth Para qué Entrada → salida
GET /status Pública ¿Está encendida? Nunca muestra montos → { enabled, reason, budgetExhausted, limits }
POST /documents Pública · 10 intentos cada 10 min y 3 documentos al día por IP Subir un PDF, DOCX o TXT (5 MB, 30 páginas) con email y autorización de datos. Primero registra el lead (demo-rag) con la prueba de la autorización; si no se puede guardar, no procesa el documento multipart: file, email, consent=true → 201 { document, limits }
GET /documents/:docId Pública Estado del documento (preguntas usadas, vencimiento) → { document }
POST /documents/:docId/questions Pública · 30 cada 10 min por IP; 10 por documento Preguntar { question (≤ 500) } → respuesta con citas
DELETE /documents/:docId Pública Borrar el documento antes de la hora → { deleted }

Códigos propios: demo_disabled, budget_exhausted, capacity (503); invalid_format (415); file_too_large (413); too_many_pages (más de 30 páginas), too_much_text (más de 400.000 caracteres aunque tenga 30 páginas o menos), empty_document, unreadable_document (422); daily_limit, question_limit, rate_limited (429); document_not_found (404); llm_error (502, la pregunta no se descuenta); lead_unavailable (503: no se pudo registrar la prueba, el documento no se procesó y el cupo del día se devuelve).

4.10 Generador para LinkedIn (/api/linkedin-ads)

Método Ruta Auth Para qué Entrada → salida
POST /generate Acceso a la demo linkedin-ads · 5 cada 10 min y 30 al día por cuenta (o por IP sin sesión) Generar post, anuncio y variantes con IA { demo: { titulo, path, … }, angulo, tono } → { data, model, usage }; 503 ai_not_configured (sin clave), budget_exhausted (cupo del mes agotado) o limits_unavailable (Redis no responde: no se puede medir el cupo), con un message que lo explica; la web sigue con su generador local; 502 llm_error o invalid_output si falla el modelo

4.11 Gestor de contenido (/api/content)

Todas exigen acceso a la demo gestor-contenido (abierta por defecto), texto de máximo 10.000 caracteres, presupuesto disponible y cupo de 20 cada 10 minutos y 100 al día por IP (el equipo no tiene cupo).

Método Ruta Para qué Entrada
POST /improve Mejorar un texto { content, template? }
POST /change-tone Cambiar el tono { content, tone, template? }
POST /adjust-length Ajustar la extensión { content, targetWords, template? }
POST /generate-versions Varias versiones { content, numVersions, template? }
POST /generate Generar desde una plantilla { template, userInput }

4.12 Documentos (/api/documents)

Todas exigen sesión y cada consulta se filtra por la cuenta que llama. Las que usan IA (subir, búsqueda semántica y explicaciones) tienen tope mensual DOCUMENTS_MONTHLY_BUDGET_USD y cupo diario por cuenta DOCUMENTS_AI_DAILY_LIMIT_PER_USER (ver 6.3); sin cupo, una subida guarda el documento sin análisis y lo dice (ai: { analyzed: false, code }).

Método Ruta Para qué Entrada
POST /upload Subir un documento (PDF, DOCX, TXT o CSV de hasta 10 MB; 10 por minuto). Extrae texto y, si hay IA, resumen y búsqueda semántica multipart: file
GET / Listar mis documentos filtros de carpeta, favoritos y papelera
POST /search/semantic Buscar por significado { query }
GET / POST /folders Carpetas { name }
GET /stats Estadísticas —
POST /:id/explain-similarity; GET /:id/explain Explicaciones con IA —
PATCH /:id Renombrar, mover, favorito —
DELETE /:id Mandar a la papelera o borrar (solo el dueño; la confirmación la pide la interfaz) ?permanent=
POST /:id/restore Sacar de la papelera —

4.13 Salud: cuentas médicas, sistema experto y herramientas internas

Estas rutas sirven a dos demos privadas (solo por invitación: Cuentas médicas y Sistema experto) y a la herramienta interna /liquidacion del equipo. Las que procesan con IA (/procesar-facturas-pdf, /procesar-modular, /aprendizaje/feedback, /api/expert/procesar* y /generar-excel, /api/cups/buscar-*, vectorizar, revectorizar, /cuentas/procesar-hibrido y /process) tienen tope mensual HEALTH_MONTHLY_BUDGET_USD y cupo diario por cuenta HEALTH_AI_DAILY_LIMIT_PER_USER (el equipo solo tiene el tope mensual). La interpretación de reglas de /api/reglas-facturacion (solo el equipo) también cuenta contra ese tope mensual.

Prefijo Rutas Auth Para qué
/api/auditoria POST /facturas, /procesar-archivos, /procesar-facturas-pdf, /procesar-modular (hasta 10 archivos de 10 MB); GET /facturas, /facturas/:id, /facturas/:id/calificar, /facturas/:id/excel, /facturas/:id/excel-auditoria-medica; DELETE /facturas/:id; POST /facturas/:id/auditar, /facturas/:id/auditar-paso-a-paso; POST /sesion/:sesionId/siguiente, GET /sesion/:sesionId; POST /soportes; GET /tarifarios; PATCH /glosas/:id; GET /estadisticas; POST /aprendizaje/feedback/:decisionId; GET /aprendizaje/estadisticas, /aprendizaje/reporte Acceso a la demo cuentas-medicas Auditoría de facturas médicas: extracción, glosas, auditoría paso a paso, Excel y calibración con retroalimentación humana
/api/documentos-conocimiento GET /config, POST /config/inicializar · PATCH /config/:documento_id · POST /config/resetear Acceso a cuentas-medicas · Equipo · Admin Documentos normativos que usa el motor de auditoría
/api/expert POST /procesar, /generar-excel, /procesar-y-descargar; GET /configuracion, /estadisticas · PUT /configuracion Acceso a la demo sistema-experto · Equipo Sistema experto de auditoría y su configuración
/api/cups GET /estadisticas, /incompletos, /estadisticas-vectorizacion; POST /buscar-semantica, /buscar-similares · POST /importar-csv, /importar-excel, /vectorizar, /revectorizar Acceso a sistema-experto · Admin Catálogo de procedimientos CUPS con búsqueda semántica; la importación solo acepta nombres de archivo de la carpeta de importación del servidor
/api (cuentas) /cuentas (CRUD), /cuentas/:id/upload (20 PDF de 50 MB), /cuentas/:id/files/:filename (DELETE y /toggle), /cuentas/procesar-hibrido, /cuentas/search/cups|medicamentos|diagnosticos|materiales, /cuentas/calcular-tarifa, /cuentas/calcular-costo-medicamentos, /ley100 (subir hasta 10 de 50 MB, listar, borrar, /toggle), POST /process, GET /export Equipo Procesamiento de cuentas médicas y documentos de la Ley 100 con salida en Excel
/api/liquidacion POST/GET /radicados, GET /radicados/:id, POST /radicados/:id/documentos (20 PDF de 50 MB), POST /radicados/:id/liquidar, GET /radicados/:id/descargar-excel, DELETE /radicados/:id, GET /estadisticas Equipo Herramienta interna de liquidación de radicados
/api/reglas-facturacion POST/GET /, GET /ejemplos, POST /previsualizar, GET/PATCH/DELETE /:id, PATCH /:id/toggle Equipo Reglas globales de facturación que usa /liquidacion

4.14 Solo desarrollo

Prefijo Auth Qué es
/api/chat (/session, /message, /history/:sessionId) Solo desarrollo Chat de eco antiguo

En producción responde 404 Endpoint not found, igual que una ruta que no existe. El diagnóstico de WhatsApp que vivía en /api/test (/whatsapp, /whatsapp/status) se retiró: lo reemplazan GET /api/admin/notifications/status y POST /api/admin/notifications/test (4.15), que también funcionan en producción.

4.15 Avisos al dueño: estado, prueba, reenvío y webhook de WhatsApp

Cada contacto nuevo (POST /api/contact, POST /api/demo-requests y la subida de «Prueba con tu documento») avisa al dueño por correo (Resend por API HTTPS; SMTP solo como respaldo) y por WhatsApp (WhatsApp Cloud API de Meta con la plantilla koptup_aviso). El envío no frena la respuesta del formulario. Estas rutas no dependen de COMMERCE_ENABLED; las de /api/admin/notifications se montan antes de /api/admin. El recorrido completo está en el diagrama 7.7 y la configuración en Operación y despliegue › 4.5.

Método Ruta Auth Para qué Entrada → salida
GET /api/admin/notifications/status admin, manager Estado de los dos canales para la tarjeta de Admin › Configuración. Nunca devuelve keys ni tokens; los destinos salen enmascarados → { email: { proveedor: "resend"|"smtp"|null, configurado, destino, remitente, ultimoExito, ultimoFallo, ultimoError, faltan }, whatsapp: { proveedor: "meta"|null, configurado, destino, plantilla, webhook, ultimoExito, ultimoFallo, ultimoError, faltan }, latido: { activo, cadaDias, ultimo, proximo } }
POST /api/admin/notifications/test admin · 5 cada 15 min por cuenta Envía de verdad un aviso de prueba por cada canal configurado. Queda en la bitácora (notifications.test) → { email: { configurado, enviado, id?, error? }, whatsapp: { configurado, enviado, id?, error?, respaldo? } }; 429 rate_limited
POST /api/admin/contacts/:id/notify admin · 10 cada 15 min por cuenta Reenviar el aviso de un contacto (ver 4.3) ?todos=1 reenvía también los canales ya enviados
GET /api/webhooks/whatsapp Pública · 600 por minuto por IP Verificación del webhook por Meta ?hub.mode=subscribe&hub.verify_token=…&hub.challenge=… → 200 con el challenge en texto si el token coincide (comparación en tiempo constante); si no, 403 forbidden
POST /api/webhooks/whatsapp Webhook firmado · 600 por minuto por IP Estados de los mensajes enviados (entry[].changes[].value.statuses[]) Cuerpo crudo de hasta 1 MB con X-Hub-Signature-256 → 200 { success: true } de inmediato y procesa después; 401 invalid_signature; 400 invalid_json

Las dos rutas del webhook responden 404 not_found mientras falte WHATSAPP_WEBHOOK_VERIFY_TOKEN o WHATSAPP_APP_SECRET. Se montan antes del parser JSON global porque la firma se calcula sobre el cuerpo crudo, y no pasan por el límite general de /api.

Estados de Contact.notificaciones.<canal>.estado:

Estado Cuándo Qué sigue
pendiente Recién creado (el envío queda «reclamado» 10 minutos) o en curso Si pasan 10 minutos sin resolverse (por ejemplo, un reinicio a mitad del envío), el job de reintentos lo retoma
enviado El proveedor lo aceptó: Resend devolvió un id o Meta un wamid (se guarda en idProveedor). También si salió con un respaldo (respaldo: onboarding@resend.dev o hello_world) Nada. En WhatsApp, el webhook lo puede pasar a entregado, leido o fallido
entregado, leido Solo WhatsApp, confirmado por el webhook Un estado nunca retrocede (un delivered tardío no pisa un read)
fallido El proveedor lo rechazó, no respondió en 15 s o Meta informó failed por el webhook. error guarda el motivo (máximo 300 caracteres, sin secretos y con los correos enmascarados) El job lo reintenta con proximoIntento: esperas de 5 min, 15 min, 1 h, 3 h y 12 h, hasta 6 intentos, solo en contactos de los últimos 3 días
no_configurado Faltaban las variables de ese canal cuando llegó el contacto (error dice cuáles) El reenvío manual lo intenta si ya se configuró

Forma de la respuesta de GET /api/admin/notifications/status (valores de ejemplo, no de producción):

{
  "success": true,
  "data": {
    "email": {
      "proveedor": "resend", "configurado": true, "destino": "du***@ejemplo.co",
      "remitente": "KopTup <avisos@koptup.com>",
      "ultimoExito": "2026-10-14T15:02:11.000Z", "ultimoFallo": null, "ultimoError": null, "faltan": []
    },
    "whatsapp": {
      "proveedor": "meta", "configurado": true, "destino": "+57 *** *** 1234",
      "plantilla": "koptup_aviso", "webhook": false,
      "ultimoExito": "2026-10-14T15:02:12.000Z", "ultimoFallo": null, "ultimoError": null, "faltan": []
    },
    "latido": { "activo": true, "cadaDias": 30, "ultimo": "2026-10-14T15:00:03.000Z", "proximo": "2026-11-13T15:00:03.000Z" }
  }
}

Con un canal sin configurar, configurado es false y faltan lista las variables (por ejemplo ["RESEND_API_KEY"] o ["WHATSAPP_API_TOKEN", "ADMIN_WHATSAPP_NUMBER"]). Si WHATSAPP_PROVIDER pide un proveedor retirado (twilio, ultramsg), proveedor es null y faltan es ["WHATSAPP_PROVIDER"].


5. Códigos de error comunes

Seis errores reales con la forma de su respuesta

Validación con mensajes por campo, API de demo sin sesión, sin acceso, sin el rol, acción repetida y enlace ya usado: respuestas reales del backend local.

HTTP code Cuándo pasa Qué hace la web
400 invalid_request Un formulario no pasa la validación. Trae fields (lista) y errores ({ campo: mensaje } en español) Marca cada campo con su mensaje
400 consent_required Falta la autorización de datos (Ley 1581) Resalta la casilla
400 invalid_json, invalid_data, bad_request Cuerpo que no es JSON, id mal formado o dato inválido Mensaje genérico
400 token_invalid, token_used, token_replaced, token_expired Enlace de activación o de recuperación que no sirve Pantalla de enlace inválido con la salida que corresponde
400 upload_limit_file_size y similares El archivo supera el tamaño o hay demasiados Mensaje en el formulario
401 unauthorized, invalid_token, session_invalid Ruta con sesión llamada sin token, con uno inválido o de una cuenta que ya no existe Lleva a /login
401 TOKEN_EXPIRED El access token venció Pide uno nuevo con /api/auth/refresh y repite
401 account_not_activated Login de una cuenta invitada que no ha usado su enlace Explica que debe activar la cuenta
401 login_required (con motivo: sin_sesion) API de una demo con solicitud o privada, sin sesión Pide iniciar sesión
401 invalid_signature Evento del webhook de WhatsApp sin firma X-Hub-Signature-256 válida — (lo recibe Meta)
403 demo_access_required (con motivo) Hay sesión pero no hay acceso vigente: sin_acceso, expirado o revocado Pantalla /demo-acceso/<slug>
403 demo_disabled La demo está desactivada en el catálogo "En mantenimiento"
403 forbidden Falta el rol —
403 requires_admin sales intenta dar o cambiar acceso a una demo privada Avisa que solo un admin puede
403 cors_forbidden Origen no permitido —
404 not_found Registro o ruta inexistente —
409 already_processed, invalid_transition, grant_revoked, active_grant_exists, account_active, not_approved La acción ya se hizo o el estado no lo permite (por ejemplo, doble clic) Refresca y muestra el estado real
409 contact_anonymized Reenviar el aviso de un contacto que ya se anonimizó El botón no aparece en esos contactos
413 payload_too_large Cuerpo de más de 2 MB —
422 team_email, fixed_mode, unknown_demo (400) Email de una cuenta del equipo, intento de cerrar el chatbot RAG o demo que no existe Mensaje en el panel
429 rate_limited Se agotó un cupo (mensaje en español); a veces trae Retry-After y retryAfterSec Pide esperar
500 internal_error Error no previsto. En producción no se muestran detalles internos Mensaje genérico
503 service_unavailable MongoDB no responde y la ruta necesita leer el rol o el acceso (falla cerrada) Página de servicio no disponible
503 ai_unavailable, ai_not_configured, budget_exhausted, limits_unavailable, demo_disabled Falta la clave de IA, Redis no responde (no se puede medir el cupo) o se agotó el tope del mes La demo pasa a su modo sin IA o lo explica
503 google_not_configured, storage_unavailable El inicio con Google no está configurado; el chatbot no tiene MongoDB Mensaje y alternativa

Los códigos de cada módulo (chatbot, demo RAG, LinkedIn) están en su sección. Una respuesta antigua sin código propio recibe el del estado HTTP (bad_request, unauthorized, forbidden, not_found, conflict, internal_error…); en producción, un 5xx nunca devuelve detalles internos.


6. Límites de uso y topes de gasto

6.1 Cupos de peticiones

Los cupos marcados "memoria" se cuentan en el proceso del backend; los marcados "Redis" se comparten entre instancias.

Dónde aplica Cupo por defecto Ventana Se cuenta por Dónde Variable
Todo /api salvo los casos de abajo 100 15 min Usuario (con token válido), sesión de refresco o IP Memoria RATE_LIMIT_MAX_REQUESTS, RATE_LIMIT_WINDOW_MS
GET /api/auth/me y GET /api/demo-access/* 120 1 min Igual que el anterior Memoria AUTH_ME_RATE_LIMIT_MAX
Todo /api/chatbot salvo GET /models 180 1 min IP Memoria CHATBOT_API_RATE_LIMIT_MAX
Preguntas al chat (POST /api/chatbot/bots/:botId/chat) 60 1 min IP Memoria CHATBOT_RATE_LIMIT_MAX
POST /api/auth/login 10 · 30 15 min IP y correo normalizado · IP Memoria LOGIN_RATE_LIMIT_MAX, LOGIN_IP_RATE_LIMIT_MAX
POST /api/auth/register 5 1 hora IP Memoria REGISTER_RATE_LIMIT_MAX
POST /api/auth/forgot-password 3 · 10 15 min IP y correo normalizado · IP Memoria FORGOT_PASSWORD_RATE_LIMIT_MAX
POST /api/auth/reset-password · POST /api/auth/activate 10 · 10 15 min IP Memoria RESET_PASSWORD_RATE_LIMIT_MAX, ACTIVATION_RATE_LIMIT_MAX
POST /api/auth/change-password 5 15 min Cuenta Memoria CHANGE_PASSWORD_RATE_LIMIT_MAX
POST /api/auth/google/exchange 20 1 min IP Memoria —
POST /api/contact 5 1 min IP Memoria —
POST /api/admin/notifications/test (aviso de prueba real) 5 15 min Cuenta Memoria —
/api/webhooks/whatsapp (no pasa por el límite general) 600 1 min IP Memoria —
POST /api/auth/activate/check 20 1 min IP Memoria —
Subidas de /api/documents/upload 10 1 min IP Memoria —
Crear bots (POST /api/chatbot/bots) 30 1 hora IP Memoria CHATBOT_CREATE_LIMIT_PER_HOUR
POST /api/demo-requests (solo envíos válidos) 5 por IP · 3 por email 1 hora · 24 horas IP · email normalizado (en hash) Redis, con respaldo en memoria DEMO_REQUEST_LIMIT_PER_HOUR, DEMO_REQUEST_LIMIT_PER_EMAIL_DAY
Demo RAG: subir documento 10 intentos · 3 documentos 10 min · día de Colombia IP Memoria · Redis —
Demo RAG: preguntar 30 · 10 por documento 10 min · vida del documento IP · documento Memoria —
POST /api/linkedin-ads/generate 5 · 30 10 min · 24 horas Cuenta (o IP sin sesión) Redis (sin Redis responde 503) —
/api/content/* (visitantes) 20 · 100 10 min · 24 horas IP Redis (sin Redis responde 503) —
Funciones con IA de /api/documents 30 24 horas Cuenta (el equipo no tiene cupo) Redis (sin Redis responde 503) DOCUMENTS_AI_DAILY_LIMIT_PER_USER
Funciones con IA de las APIs de salud 20 24 horas Cuenta (el equipo no tiene cupo) Redis (sin Redis responde 503) HEALTH_AI_DAILY_LIMIT_PER_USER

Cada ruta de cuenta tiene su propio contador: agotar el del login no bloquea el registro, la recuperación ni la activación. Los cupos por correo cuentan el correo normalizado (mayúsculas, puntos y +etiqueta de Gmail no abren un contador nuevo). Todas las respuestas 429 traen code: "rate_limited", un mensaje en español y la cabecera Retry-After.

Detrás del proxy de Railway el servidor confía en un salto (TRUST_PROXY_HOPS, por defecto 1) para leer la IP real del visitante. Los cupos estándar se ven en las cabeceras RateLimit-* de cada respuesta:

Cabeceras de cupo y límites públicos de la demo RAG

La captura es anterior a la verificación final del 14 de octubre de 2026: hoy el chatbot informa 180 por minuto en sus rutas y 60 por minuto en las preguntas. La verificación de sesión informa 120 por minuto y la demo RAG publica sus límites en /status.

6.2 Tamaños máximos

Qué Límite
Cuerpo JSON o de formulario 2 MB (15 MB solo para subir documentos a un bot)
Documentos del gestor (/api/documents/upload) 10 MB (MAX_FILE_SIZE), 1 archivo; PDF, DOCX, TXT, CSV
Adjuntos de un pedido 20 MB por archivo, 10 por pedido; documentos, imágenes, ZIP y RAR
Demo RAG 5 MB y 30 páginas; PDF, DOCX, TXT
Bots del chatbot 5 archivos de 5 MB por envío, 50 documentos y 5.000 fragmentos por bot, 5 URL por envío (2 MB, 8 s)
Auditoría médica 10 MB por archivo, 10 por envío
Cuentas médicas, Ley 100 y liquidación 50 MB por PDF; 20, 10 y 20 archivos por envío

6.3 Topes de gasto de IA

Seis funciones con IA tienen un tope mensual en dólares. El gasto se calcula con los tokens que informa el proveedor, se suma en Redis por mes (UTC) y la API nunca devuelve montos.

Función Variable Tope por defecto Clave en Redis Cuando se agota
Demo RAG "Prueba con tu documento" DEMO_MONTHLY_BUDGET_USD USD 50 demo-rag:spend:AAAA-MM La demo se apaga: 503 budget_exhausted
Chatbot RAG CHATBOT_MONTHLY_BUDGET_USD USD 50 chatbot:spend:AAAA-MM Responde en modo extractivo (cita fragmentos sin IA)
Generador para LinkedIn LINKEDIN_ADS_MONTHLY_BUDGET_USD USD 20 linkedin-ads:spend:AAAA-MM 503 budget_exhausted; la web usa su generador local
Gestor de contenido CONTENT_MONTHLY_BUDGET_USD USD 20 content:spend:AAAA-MM 503 budget_exhausted
Gestor de documentos (/api/documents) DOCUMENTS_MONTHLY_BUDGET_USD USD 10 documents:spend:AAAA-MM 503 budget_exhausted; una subida se guarda sin análisis
APIs de salud (cuentas médicas, auditoría, sistema experto, CUPS y reglas de facturación) HEALTH_MONTHLY_BUDGET_USD USD 30 health:spend:AAAA-MM 503 budget_exhausted
flowchart LR
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef no fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef dec fill:#fef3c7,stroke:#d97706,color:#78350f

    A["Petición que usaría IA"] --> B{"Hay clave de OpenAI?"}:::dec
    B -- no --> X1["503 o modo sin IA"]:::no
    B -- "sí" --> C{"Redis responde?"}:::dec
    C -- no --> X2["Falla cerrada: 503 o modo sin IA"]:::no
    C -- "sí" --> D{"Gasto del mes menor al tope?"}:::dec
    D -- no --> X3["budget_exhausted"]:::no
    D -- "sí" --> E{"Dentro del cupo?"}:::dec
    E -- no --> X4["429 rate_limited"]:::no
    E -- "sí" --> F["Llama al modelo y suma el costo al mes"]:::ok
Loading

Si no se puede medir el gasto, no se gasta: sin Redis, las funciones con IA públicas se apagan o pasan a su modo sin IA.

Un tope con valor inválido o negativo deja la función sin presupuesto (cero). En documentos y salud, el cliente de OpenAI de cada función suma el costo real de cada respuesta y no llama al modelo con el tope agotado o sin Redis, aunque el servicio que lo use no pase por la ruta con cupo.


7. Flujos clave paso a paso

Los cuatro diagramas siguientes cubren el recorrido completo solicitud → aprobación → activación → acceso. El mismo recorrido, visto por el usuario, está en Flujos del prospecto y cliente. El 7.7 muestra el aviso al dueño de un contacto nuevo, con sus reintentos y el webhook de WhatsApp.

7.1 Solicitud pública

sequenceDiagram
    autonumber
    actor V as Visitante
    participant W as Web
    participant A as API
    participant R as Redis
    participant M as MongoDB
    actor D as Dueño
    V->>W: Llena Solicitar demo y autoriza sus datos
    W->>A: POST /api/demo-requests
    alt Campo trampa lleno (bot)
        A-->>W: 201 con un código, sin guardar nada
    else Sin autorización de datos
        A-->>W: 400 consent_required
    else Datos inválidos
        A-->>W: 400 invalid_request con errores por campo
    else Datos válidos
        A->>R: Suma 1 al cupo de la IP y al del email
        alt Cupo agotado
            A-->>W: 429 rate_limited con Retry-After
        else Hay cupo
            A->>M: Busca una solicitud abierta del mismo email (30 días)
            alt Ya existe
                A->>M: Le suma las demos y anota el reenvío
                A-->>W: 201 fusionada true
            else Es nueva
                A->>M: Crea DemoRequest pendiente con la prueba del consentimiento
                A-->>W: 201 codigo y estado pendiente
                A->>M: Registra el lead en Contact (demo-request)
                A-->>V: Acuse por correo (si hay correo configurado)
                A--)D: Aviso al dueño por correo y WhatsApp, sin esperar
            end
        end
    end
Loading

7.2 Aprobación por el equipo

sequenceDiagram
    autonumber
    actor E as Equipo
    participant A as API
    participant M as MongoDB
    participant S as Correo
    E->>A: POST /api/admin/demo-requests/:id/approve
    A->>M: Lee el rol vigente del que aprueba
    alt Demo privada y no es admin
        A-->>E: 403 requires_admin
    else Puede aprobar
        A->>M: Cambia pendiente o en_revision a aprobada (condicionado)
        alt Otro ya la procesó
            A-->>E: 409 already_processed
        else La reclamó
            A->>M: Vincula la cuenta del email o crea una prospect invitada
            A->>M: Crea un DemoGrant por demo (o extiende el vigente)
            A->>M: Emite el enlace de activación (72 h, solo se guarda su hash)
            A->>S: Correo con el enlace si hay correo configurado (espera hasta 15 s)
            A->>M: Registra demo_request.approve en la bitácora
            A-->>E: 200 con activationUrl, accesos y si el correo salió
        end
    end
Loading

Si algo falla a mitad de camino, la API deshace lo que creó y devuelve la solicitud a su estado anterior.

7.3 Activación de la cuenta

sequenceDiagram
    autonumber
    actor P as Prospecto
    participant W as Web
    participant A as API
    participant M as MongoDB
    participant R as Redis
    P->>W: Abre /activar/TOKEN
    W->>A: POST /api/auth/activate/check
    A->>M: Busca el hash del token sin gastarlo
    A-->>W: Nombre y correo enmascarado, o token_invalid, token_used, token_replaced, token_expired
    P->>W: Escribe su contraseña
    W->>A: POST /api/auth/activate
    A->>M: Marca el enlace como usado (atómico, un solo uso)
    A->>M: Guarda la contraseña cifrada y pasa la cuenta a activo
    A->>M: Invalida otros enlaces pendientes y registra user.activate
    A->>R: Guarda el refresh token (7 días)
    A-->>W: accessToken, refreshToken y usuario
    W-->>P: Mis demos con aviso de bienvenida
Loading

7.4 Acceso a una demo

sequenceDiagram
    autonumber
    actor P as Prospecto
    participant W as Servidor de la web
    participant A as API
    participant M as MongoDB
    P->>W: Abre /demo/erp
    W->>A: GET /api/demo-access/erp con su token
    A->>M: Catálogo (modo y activa) y DemoGrant de la cuenta
    alt Acceso vigente
        A->>M: Suma una visita si pasaron 30 min desde la última
        A-->>W: allowed true, reason grant, días restantes
        W-->>P: Muestra la demo
        P->>A: Llamadas a la API de la demo, por ejemplo /api/linkedin-ads/generate
        A->>M: Repite la revisión del acceso en cada llamada
        A-->>P: Respuesta de la demo
    else Sin acceso, vencido o revocado
        A-->>W: allowed false con el motivo
        W-->>P: Pantalla /demo-acceso/erp con la salida adecuada
    end
Loading

7.5 Sesión: login, renovación y cierre

sequenceDiagram
    autonumber
    participant W as Web
    participant A as API
    participant R as Redis
    W->>A: POST /api/auth/login
    A->>R: Guarda refresh_token de la cuenta (7 días, reemplaza el anterior)
    A-->>W: accessToken de 15 min y refreshToken
    W->>A: Peticiones con Authorization Bearer
    A-->>W: 401 TOKEN_EXPIRED cuando vence
    W->>A: POST /api/auth/refresh
    A->>R: Compara con el guardado
    A-->>W: accessToken nuevo
    W->>A: POST /api/auth/logout
    A->>R: Borra el refresh token
Loading

7.6 Vencimiento automático de los accesos

sequenceDiagram
    autonumber
    participant J as Proceso horario
    participant R as Redis
    participant M as MongoDB
    participant S as Correo
    J->>R: Toma el candado jobs:demo-grants:lock
    alt Otra instancia lo tiene
        J-->>J: Salta esta corrida
    else Lo toma (o Redis no responde)
        J->>M: Marca expirado los accesos activos con expiresAt pasado
        opt Hay correo configurado
            J->>M: Reclama los que vencen en 3 días sin recordatorio
            J->>S: Un recordatorio por persona
        end
        J->>R: Suelta el candado
    end
Loading

Corre cada hora (DEMO_GRANTS_JOB_INTERVAL_MS), la primera vez unos 30 segundos después de arrancar; DEMO_GRANTS_JOB_ENABLED=false lo apaga. La vigencia real no depende de él: la API compara la fecha en cada petición.

7.7 Aviso al dueño de un contacto nuevo

sequenceDiagram
    autonumber
    actor V as Visitante
    participant A as API
    participant M as MongoDB
    participant RS as Resend
    participant ME as Meta WhatsApp
    actor D as Dueño
    V->>A: POST /api/contact (o Solicitar demo, o Prueba con tu documento)
    A->>M: Crea el Contact con cada canal pendiente o no_configurado
    A-->>V: 200 sin esperar los avisos
    par Correo
        A->>RS: POST /emails con Idempotency-Key contact-id-email-hash del cuerpo y reply_to
        alt 2xx con id
            RS-->>A: id del correo
            A->>M: email enviado con idProveedor
            RS--)D: Correo a NOTIFY_EMAIL
        else Error o sin respuesta en 15 s
            A->>M: email fallido con error y proximoIntento
        end
    and WhatsApp
        A->>ME: POST /v25.0/id-del-número/messages con la plantilla koptup_aviso
        alt 2xx con wamid
            ME-->>A: wamid
            A->>M: whatsapp enviado con idProveedor
            ME--)D: WhatsApp al número del dueño
        else Error de plantilla 132xxx
            A->>ME: Plantilla de respaldo hello_world
            A->>M: whatsapp enviado con respaldo hello_world y el error
        else Otro error
            A->>M: whatsapp fallido con el código de Meta
        end
    end
    loop Cada 5 min, contactos de los últimos 3 días
        A->>M: Reclama los canales fallidos con proximoIntento vencido
        A->>RS: Reintento (misma Idempotency-Key si el cuerpo no cambió)
        A->>ME: Reintento de la plantilla
    end
    opt Webhook configurado
        ME->>A: POST /api/webhooks/whatsapp firmado
        A->>M: Busca por wamid y pasa a entregado, leido o fallido
        A--)D: Si falló, correo de alerta con el código de Meta
    end
Loading

El formulario responde sin esperar a los proveedores. Cada canal se envía y se guarda por separado; un reintento nunca toca un canal que ya salió. Antes de cada intento, el backend «reclama» el canal con una actualización condicionada, así que dos procesos nunca envían el mismo aviso a la vez. Una vez al mes, el job del latido manda un aviso de prueba por los dos canales.


8. Ejemplos con curl

Usa tu backend local y guarda la URL en una variable. Las respuestas son reales (recortadas donde dice …).

export API=http://localhost:3001

8.1 Leer el catálogo (pública)

curl -s "$API/api/demo-catalog"
{
  "success": true,
  "fuente": "bd",
  "data": [
    { "slug": "chatbot", "nombre": "Chatbot RAG con tus documentos", "nombreEn": "RAG chatbot over your documents", "accessMode": "publico", "activo": true },
    { "slug": "code-review-ia", "nombre": "Revisión de código con IA", "nombreEn": "AI code review", "accessMode": "publico", "activo": true },
    …
  ]
}

8.2 Solicitar una demo (pública)

curl -s -X POST "$API/api/demo-requests" \
  -H 'Content-Type: application/json' \
  -d '{
    "nombre": "Laura Martínez",
    "empresa": "Agroinsumos del Llano SAS",
    "cargo": "Gerente administrativa",
    "email": "laura.martinez@ejemplo.co",
    "telefono": "+57 310 555 0101",
    "pais": "Colombia",
    "tamanoEmpresa": "11-50",
    "demos": ["erp", "linkedin-ads"],
    "casoDeUso": "Queremos controlar inventario y facturación de nuestras tres bodegas en un solo sistema.",
    "consentimiento": true,
    "origen": { "pagina": "/solicitar-demo", "utm": { "source": "linkedin", "campaign": "erp-pymes" } }
  }'
{
  "success": true,
  "message": "Recibimos tu solicitud. Te escribiremos al correo que indicaste.",
  "data": { "codigo": "DR-2026-6T8CKS", "estado": "pendiente", "fusionada": false }
}

Si falta la autorización de datos la respuesta es 400:

{
  "fields": ["consentimiento"],
  "success": false,
  "code": "consent_required",
  "message": "Debes autorizar el tratamiento de tus datos personales (Ley 1581 de 2012) para enviar la solicitud."
}

8.3 Iniciar sesión y ver Mis demos

# 1. Inicia sesión (la contraseña es la que la persona creó al activar su cuenta)
curl -s -X POST "$API/api/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "laura.martinez@ejemplo.co", "password": "<tu contraseña>" }'
{
  "success": true,
  "message": "Login successful",
  "data": {
    "accessToken": "<oculto>",
    "refreshToken": "<oculto>",
    "user": { "id": "6ac89e19d28776d0718d6513", "email": "laura.martinez@ejemplo.co", "name": "Laura Martínez", "role": "prospect" }
  }
}
# 2. Copia el accessToken en una variable y pide tus demos
export TOKEN='<accessToken>'
curl -s "$API/api/me/demos" -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "data": [
    {
      "id": "6ac89e19d28776d0718d6516",
      "demoSlug": "erp",
      "demoNombre": "ERP modular",
      "accessMode": "solicitud",
      "demoActiva": true,
      "estado": "activo",
      "vigente": true,
      "expiresAt": "2026-10-23T07:56:09.487Z",
      "diasRestantes": 14,
      "ultimoAcceso": "2026-10-09T07:56:10.018Z",
      "accesos": 1,
      "url": "/demo/erp"
    },
    …
  ]
}

Con el mismo token, una API de una demo a la que la cuenta no tiene acceso responde 403:

curl -s "$API/api/auditoria/estadisticas" -H "Authorization: Bearer $TOKEN"
{
  "success": false,
  "code": "demo_access_required",
  "demo": "cuentas-medicas",
  "motivo": "sin_acceso",
  "message": "Tu cuenta no tiene acceso a esta demo. Solicítalo desde la página de la demo."
}

9. Salud del servicio y documentación interactiva

Respuestas reales de /health/live y /health

/health/live dice si el proceso está vivo; /health dice si puede atender (MongoDB conectado).

  • /health/live responde 200 siempre que el proceso esté vivo, aunque MongoDB esté caído: sirve para diagnosticar si el proceso arranca.
  • /health responde 503 mientras MongoDB no esté conectado. Es el healthcheck del despliegue en Railway (un despliegue sin base no entra en servicio y la versión anterior sigue atendiendo) y lo revisa el monitoreo de GitHub Actions cada 15 minutos. Si el backend arranca sin base de datos, sigue vivo y las rutas que necesitan leer roles o accesos responden 503 service_unavailable (falla cerrada).
  • Swagger (/api-docs) existe solo fuera de producción o con API_DOCS_ENABLED=true. Se genera con los comentarios @swagger de src/routes/*.ts y config/swagger.ts le agrega el esquema de sesión (bearerAuth), qué rutas son públicas, las de solo desarrollo (marcadas como obsoletas) y el formato de error. Los comentarios no cubren todas las rutas: esta página es la referencia completa.

Qué hacer cuando el backend no responde está en Operación y despliegue.


10. Para desarrolladores

Qué Dónde (en apps/backend/src)
Arranque: variables, carpetas de subida, MongoDB, admin desde ADMIN_EMAIL (services/admin-bootstrap.service.ts), semilla del catálogo, Passport, jobs index.ts
Aplicación Express (middleware y montaje de rutas) app.ts
Políticas de autorización middleware/access.ts y el inventario routes/POLICIES.md
Sesión JWT middleware/auth.ts, controllers/auth.controller.ts
Errores y su traducción a { success: false, code, message } middleware/errorHandler.ts (excepciones) y utils/http-error.ts (sendError y el middleware que completa las respuestas antiguas)
Cupos middleware/rateLimiter.ts, middleware/client-ip.ts, services/request-limits.service.ts
Topes de gasto de IA y cupos en Redis services/ai-budget.service.ts
Sistema de demos routes/demo-public.routes.ts, routes/admin-demos.routes.ts, services/demo-*.service.ts, services/magic-link.service.ts, jobs/demo-grants.job.ts, config/demos.ts
Avisos al dueño (correo y WhatsApp) services/owner-notify.service.ts, services/email.service.ts (Resend y SMTP), services/whatsapp.service.ts (plantilla de Meta), routes/admin-notifications.routes.ts, routes/whatsapp-webhook.routes.ts (montado en app.ts antes del parser JSON), jobs/notify-retry.job.ts, jobs/notify-heartbeat.job.ts, models/Contact.ts (notificaciones)
Pruebas de la API (401/403/200 por política, flujo de demos) __tests__/integration/ con supertest

Checklist para agregar una ruta:

  1. Elige la política (sección 3) y aplícala en el archivo de rutas con los middleware de middleware/access.ts. Si la ruta es del equipo, usa requireRole (lee el rol en la base) y no solo el rol del token.
  2. Valida la entrada (en el código nuevo se usa zod y zodToAppError para devolver fields y errores).
  3. Lanza AppError(mensaje, status, code) o usa sendError(res, status, code, mensaje) en vez de armar el JSON a mano: así la respuesta tiene siempre success, code y un message en español.
  4. Envuelve los controladores asíncronos con asyncHandler.
  5. Si llama a OpenAI desde una ruta pública, pasa por getBudgetStatus y recordSpend y agrega un cupo en Redis.
  6. Si la acción la hace el equipo, regístrala con recordAudit.
  7. Agrega la ruta a routes/POLICIES.md y una prueba de integración.

11. Limitaciones conocidas

Tema Qué pasa hoy
Crear entregables, facturas y conversaciones POST /api/deliverables, POST /api/invoices, POST /api/admin/orders/:orderId/invoice y POST /api/messages/conversations responden 500 en main: los modelos exigen el número consecutivo (DEL-…, FAC-…, CONV-…) antes de que se ejecute el código que lo genera. Por eso hoy no se pueden crear entregables, facturas ni conversaciones por API; el modelo de mensajes tiene el mismo patrón
Conversación automática de un pedido Al crear un pedido, la conversación con el equipo no se crea (el error se registra en el log y el pedido sí se guarda)
Pagos No hay pago en línea de facturas: el portal muestra los datos de transferencia que configure el equipo y el equipo registra el pago con POST /api/invoices/:id/pay (admin o manager). Los pagos en línea están en desarrollo (ver Estado y próximos pasos)
Descargar factura GET /api/invoices/:id/download devuelve un enlace a sí misma; no se genera un PDF
Adjuntos de pedidos Se guardan en el disco del servidor: sin un volumen en Railway se borran en cada despliegue y la descarga responde 410 file_unavailable (acciones pendientes del dueño)
Notificaciones Ningún flujo las crea solo; solo existen las que se crean con POST /api/notifications
Cupos en memoria El cupo general, los de cada ruta de cuenta y el del chatbot se reinician en cada despliegue y no se comparten si hay varias instancias
Una sesión renovable por cuenta El refresh token es uno por cuenta: iniciar sesión en otro navegador hace que el primero no pueda renovar y deba volver a entrar a los 15 minutos
Swagger incompleto Los comentarios @swagger no cubren todas las rutas
Avisos por WhatsApp sin webhook Sin WHATSAPP_APP_SECRET y WHATSAPP_WEBHOOK_VERIFY_TOKEN, el estado queda en enviado (Meta aceptó el mensaje) y nunca pasa a entregado, leido ni fallido

12. Páginas relacionadas

Inicio · Estado y próximos pasos · Correcciones pendientes

Documentación (lo que existe hoy)

Estrategia

Cliente y demos

Sitio

Productos

Plataforma

Roadmap

Clone this wiki locally