Yurest API · v1

Documentación de la API

Endpoints REST expuestos por n8n para el portal de gestión Yurest. Base URL: {BASE_URL}

Revisada el 20-oct-2026 contra config.js, js-cliente/ y los workflows de n8n.

Introducción

Esta API se expone vía workflows de n8n y persiste en Postgres self-host (ex-Supabase). Todos los endpoints comparten el prefijo:

{BASE_URL}

{BASE_URL} es la URL de webhooks del servidor n8n de Yurest seguida de /webhook. En el portal sale de js/config.js; en esta documentación se escribe así para no publicar el host.

El frontend usa YurestConfig.apiFetch(url, opts), que añade automáticamente la cabecera X-Session-Token (vía getAuthHeaders()), deduplica GETs idénticos en vuelo, aplica un timeout de 30 s (override con timeoutMs) y maneja sesiones expiradas (401/403 → limpia sesión y redirige a login, salvo skipAuthLogout:true).

Convención de respuesta: casi todos los endpoints devuelven 200 OK con un objeto JSON. Operaciones de escritura devuelven al menos { success: boolean } y, donde aplica, datos de diagnóstico (filas afectadas, errores específicos).
Dos superficies, no una. Todo lo de abajo es la API del portal de staff. El Espacio Cliente habla con otra base y otra cabecera — ver 🏠 Espacio Cliente.

Autenticación

Basic Auth RETIRADO (auditoría jun-2026). La credencial compartida Authorization: Basic fue eliminada del cliente y rotada. Ya no se envía. Todos los webhooks validan ahora un token de sesión firmado (HMAC-SHA256).

El login lo emite el workflow WF16 (auth/login). Cada petición del portal lo manda en la cabecera X-Session-Token, que añade automáticamente YurestConfig.apiFetch() a través de getAuthHeaders(). Cada workflow valida el token server-side (el «Auth Gate HMAC» inline, ~51 workflows migrados).

X-Session-Token: <userId>|<exp>.<hmac>
Content-Type: application/json

Formato del token

ParteContenido
userIdID del usuario autenticado (tabla usuarios).
expEpoch de expiración. TTL 30 días si «recordar», 8 h si no.
hmacHMAC_SHA256(userId|exp, SECRET). El secreto vive solo en n8n.
Modo Laravel (migración en curso): si localStorage.yurest_backend === 'laravel', getAuthHeaders() manda en su lugar Authorization: Bearer <token> (Sanctum) + Accept: application/json y reescribe la URL al backend nuevo (api.yurest.com). Los endpoints sin mapping degradan a n8n. Ojo: el flag solo se respeta en desarrollo local — config.js lleva una guarda que fuerza n8n en producción, porque api.yurest.com aún no está desplegado y sin ella un yurest_backend='laravel' olvidado dejaría el portal muerto. Por defecto el portal sigue contra n8n con X-Session-Token.

Si la sesión expira, cualquier endpoint responde 401/403; apiFetch limpia la sesión y redirige a login.html. Las peticiones de fondo pasan { skipAuthLogout:true } para no expulsar al usuario ante un 401 best-effort.

Endpoints públicos (sin sesión)

El cliente final no tiene cuenta; estos endpoints se autorizan por token aleatorio incrustado en el enlace del email, no por sesión.

EndpointAutorizaciónPor qué es público
auth/loginusuario + contraseñaEmite el token de sesión.
responderSolicitudaccess_tokenEl cliente rellena la ficha desde el email.
completarFicha?t=<token>token de solicitudPrecarga datos del formulario del cliente.
oferta-publica?t=<token>token de ofertaVista pública de la oferta (configurador).
pago/iniciartoken de ofertaEl cliente paga desde la oferta pública (Redsys).
portal-cliente/*token propio del clienteAPI del Espacio Cliente: base y token distintos, ver su sección.
oferta-cambios (POST)token de ofertaTelemetría de cambios del cliente en la oferta (fire-and-forget).
carga-datos/confirmar-email (POST)firma HMAC del enlaceEl cliente confirma desde el botón del correo de su carpeta que ya subió los documentos (WF-116).
bienvenida/huecos · bienvenida/reservar (POST)token de solicitud (t)El cliente reserva su sesión de bienvenida grupal (WF-124). Desde el 24-oct-2026 el enlace se comprueba en la BD antes de leer el calendario: sin enlace válido → { ok:false, error:'enlace_invalido' } sin tocar Google.
sepa/mandato (GET) · sepa/firmar (POST)token HMAC de re-firmaEl cliente vuelve a firmar un mandato SEPA desde firmar-sepa.html (WF70).
agenda-privadas/estado · agenda-privadas/reservar (POST)token del proyecto (t)El cliente grupal agenda sus dos sesiones privadas desde agendar-sesion.html (WF-138).
escalado-local/solicitud · escalado-local/responder (POST)token del escalado (t)El cliente rellena los datos de su local nuevo desde escalado-local.html (WF-142).

Errores y diagnóstico

StatusSignificadoCuándo
200OK · revisa body.successCasi todas las respuestas (incluso fallos lógicos vienen 200).
401 / 403Sesión expiradaFrontend redirige a login.html.
500Error interno del workflowExcepción no controlada en n8n.
Patrón de diagnóstico: los workflows de escritura críticos (eliminarFicha, grabadoA3) devuelven además { success, affected/ficha_count/sol_count, errores: [] } para detectar UPDATEs sin efecto (RLS bloqueando, ID inexistente, etc.).

CORS

Todos los endpoints aceptan peticiones desde cualquier origen y responden con los siguientes headers:

Access-Control-Allow-Origin:  *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Session-Token

Las rutas del Espacio Cliente aceptan además X-Cliente-Token y X-Cliente-Proyecto.

🔑 Auth y usuarios

POST /webhook/auth/login Público

Workflow WF16. Valida credenciales contra la tabla usuarios y, si son correctas, emite el token de sesión firmado (HMAC) que el resto de la API exige.

Body

{ "username": "usuario@ejemplo.com", "password": "..." }

Respuesta 200 (éxito)

{
  "success": true,
  "user": {
    "id": "uuid",
    "username": "usuario@ejemplo.com",
    "nombre": "Ana García",
    "email": "usuario@ejemplo.com",
    "rol": "admin | user",
    "token": "<userId>|<exp>.<hmac>",
    "permisos": { "read": [...], "write": [...], "delete": [...] }
  }
}
El front valida estrictamente success && user.username && user.rol. Credenciales inválidas → mensaje neutro (no distingue usuario inexistente de contraseña incorrecta).
GET /webhook/auth/verify?userId=<id> X-Session-Token

Verifica que la sesión sigue viva (chequeo periódico del portal). Un 401/403 fuerza logout.

GET POST /webhook/auth/usuarios · /webhook/auth/roles X-Session-Token

Gestión de usuarios y roles (Administración → admin.html). auth/usuarios lista/crea/edita/borra usuarios y sus permisos; auth/roles gestiona los roles base. Acciones vía action en el body. El rol se calcula como base + deltas granulares { read, write, delete }.

EndpointUso
auth/usuariosCRUD de usuarios + permisos (create/update/password/delete).
auth/rolesCatálogo de roles base y sus permisos por defecto.
auth/usuarios/historialGET del audit log de acciones sobre usuarios (pestaña «Historial»). Tabla usuarios_historial.

📋 Fichas

GET /webhook/018f3362-7969-4c49-9088-c78e4446c77f X-Session-Token

Workflow 04-fichas-alta. Devuelve la lista completa de fichas (con su cs_estado del Kanban CS). Alias en config.js: ENDPOINTS.login · ENDPOINTS.altas.

Respuesta 200

{
  "clientes": [
    {
      "id": "uuid",
      "denominacion": "Test SL",
      "cif": "B40597437",
      "email": "contacto@test.es",
      "tipo_cliente": "lite | planes | corporate",
      "estado": "rellenado | completada",
      "cs_estado": "en_implementacion | post_primer_mes | ...",
      "grabado_a3": false,
      "grabado_a3_at": null,
      "introducido_yurest": false,
      "promocion_id": null,
      "fecha_solicitud": "2026-04-17T...",
      "fecha_rellenado": "2026-04-17T...",
      "fecha_completado": "2026-04-17T...",
      "sepa_mandato": { ... },
      "adjuntos": [ ... ],
      "paquetes_carrito": { ... },
      "deleted_at": null
      // ... resto de columnas
    }
  ]
}
Filtrado backend

El workflow excluye filas con deleted_at != NULL. Devuelve todas las columnas, incluidas las vacías como "".

POST /webhook/57e04029-bae4-4124-8c43-c535e831a147 X-Session-Token

Workflow 04-fichas-alta. Crea una ficha nueva (sin id) o actualiza una existente (con id). En modo edición solo actualiza los campos enviados — no destruye los demás.

Body (todos opcionales en edición)

CampoTipoNotas
iduuidSi presente → UPDATE; ausente → INSERT
Nombre SociedadstringMapea a denominacion
CIF/NIFstringMapea a cif
Email · Email Factura · Email CCstring
Tipo Clientestringlite · planes · corporate
Estadostring"rellenado" cuando consultor completa
JP * · Firmante * · TPV *stringsBloques de datos
Lite · Distribuidor"Sí" / ""Booleanos como string
AdjuntosarrayPDF/JPG/PNG en base64
SEPAobjectMandato firmado (datos + firma_base64)
paquetes_carritoobjectSnapshot del carrito (módulos, precios, periodos)
localesarrayLocales de la ficha (insertados aparte)

Respuesta 200 (éxito)

{ "id": "uuid-de-la-ficha", "success": true }

Respuesta 200 (error en INSERT Locales)

{ "error": "..." }

El nodo INSERT Locales tiene continueOnFail: true, así que un fallo en locales no rompe el INSERT/UPDATE de la ficha principal pero sí aparece en el body.

GET /webhook/5a304fcd-ae1d-49e6-92d1-c5a5e007bbfd?t=<token>|?id=<id> Público

Workflow 12-completar-ficha. Devuelve los datos de una ficha o solicitud para precargar el formulario. Acepta tres formas de identificación.

Query params

ParamTipoUso
tstring (16-128 chars)Token aleatorio público (preferente)
iduuid o númeroUUID de ficha o ID-Solicitud numérico (uso interno)

Respuesta 200 (encontrado)

{
  "found": true,
  "id": "uuid",
  "ID Solicitud": 5,
  "Nombre Sociedad": "Test SL",
  "Email": "contacto@test.es",
  "Tipo Cliente": "planes",
  "SEPA": { "iban": "ES...", "deudor": {...}, "firma_base64": "..." },
  // ... resto de campos
}

Respuesta 200 (no encontrado)

{ "found": false, "error": "Solicitud no encontrada" }
POST /webhook/a2b1b1d6-a1dc-4366-b60e-b5e4506faa3d X-Session-Token

Workflow 10-eliminar. Soft-delete (UPDATE deleted_at = NOW()) en fichas_alta y solicitudes. Como los UUIDs son únicos entre tablas, sólo afecta a la que contiene el ID.

Body

{
  "action": "delete",
  "entity": "ficha" | "solicitud",
  "id": "uuid",
  "row_number": "uuid"   // alias por compat
}

Respuesta 200

{
  "success": true,
  "entity": "ficha",
  "id": "uuid",
  "ficha_count": 1,
  "sol_count": 0,
  "errores": []
}
success: false: revisa errores (problemas Supabase) o si ficha_count + sol_count == 0 → el id ya no existía.
POST varios · ver tabla X-Session-Token

Acciones puntuales sobre una ficha ya creada (no re-guardan la ficha entera). Todas reciben { ficha_id } y devuelven { ok, ... }.

PathWorkflowEfecto
/ficha/notificar-completaWF19Al completar la ficha: email Drive al cliente + tarea Asana + email integraciones. Antes vivía en WF11.
/ficha/introducir-yurestWF64Flag «Introducida en Yurest» (idempotente). 1ª vez envía aviso. → { ok, introducido_yurest, introducido_yurest_at }.
/ficha/marcar-ganado-hlWF65{ ficha_id, marcar_ganado:true } → mueve la oportunidad HighLevel a won + «Ganados Planes» y escribe MRR/ARR/nº locales/periodicidad. Desde el 8-oct-2026 rellena además el id de cliente en producto (id_bbdd) del Business, buscado por CIF, solo si está vacío y ningún otro Business lo lleva ya; nunca pisa un valor. Si la ficha no tiene contacto de HighLevel no toca ninguna oportunidad.
/contrato-enviarWF84Envía el contrato a firmar (efirma) → { ok, id_operacion, destinatarios }. Con { preview:true } devuelve el HTML sin enviar.
/fichas-adjuntos-uploadWF35{ nombre, tipo, data(dataURL), ficha_id? } → sube al bucket Storage. → { storage_path, signed_url, size }.
/sepa/solicitar-firmaWF70{ ficha_id, mandato_id, email } → envía al cliente enlace a firmar-sepa.html (token HMAC).
/fichas/importar-sheetWF54{ sheetUrl } → importa ficha desde Google Sheets. → { ok, ficha, avisos }.
GET POST /webhook/cliente-notas · /cs-estado · /ficha-asignar-promo X-Session-Token

Modal «Ver cliente» (clientes.html): notas internas, movimiento en el Kanban CS y asignación de promoción.

PathMétodoUso
/cliente-notasGET ?ficha_id= / POSTNotas internas firmadas con el email del JWT. GET → { notas:[{id,autor_email,texto,created_at}] }; POST { ficha_id, texto }.
/cliente-notas-editar · /cliente-notas-borrarPOSTEditar / borrar una nota.
/cs-estadoPOSTMueve el cliente entre columnas del Kanban CS. Actualiza cs_estado + cs_estado_historial.
/ficha-asignar-promoPOST{ ficha_id, promo_id, promo_turno } → patch de promocion_id/promocion_turno/promo_asignada sin re-guardar la ficha. Si la ficha tiene carga_datos_task_gid, renombra la tarea de carga en Asana con « · Promo <nombre> (<turno>)» (la quita al desasignar; no toca el resto del nombre). Un fallo de Asana no afecta a la asignación.
POST /webhook/importador/highlevel X-Session-Token

Workflow WF-132. Pestaña HighLevel del importador de clientes (importador-clientes.html). Solo lee de HighLevel y del portal: la ficha la guarda después el flujo normal (guardarFicha). La conversión HL → ficha es js/lib/parse-ficha-hl.js, inlineada en el workflow.

opBodyRespuesta
buscar{ op, q } (≥ 2 letras){ ok, items:[{ id, nombre, sociedad, cif, tipo, ciudad, id_bbdd, ficha|null }] }. ficha no nulo = ya está en el portal.
ficha{ op, business_id }{ ok, ficha, avisos[], resumen, existente|null }. resumen: business_id, nombre, cif, tipo, locales, locales_inactivos, contactos, contactos_lista[{ …, clave }], venta, cs, escalados, jp, citas, promo, duplicados, notas, sesiones. Error: { ok:false, code:'sin_business' }.
vincular{ op, ficha_id, business_id, locales:[{ nombre, highlevel_record_id }] }{ ok, n }. Tras guardar la ficha, ancla el Business y los locales de HighLevel. Solo rellena vacíos: nunca pisa un vínculo.
Clientes de baja (etapa churn/baja o Business inactivo): se importan también sus locales inactivos, para conservar el historial. La mensualidad de los locales no se importa (el MRR de HighLevel no es fiable). Si HighLevel limita las lecturas (429), se avisa en avisos.

📨 Solicitudes

POST /webhook/b0629324-e611-47d4-835f-3ac9bcd4dc9b X-Session-Token

Workflow 08-solicitudes. Inserta solicitud en estado pendiente y envía email al cliente con la URL del formulario.

Body

{
  "ID Solicitud": 5,
  "access_token": "32-chars-uuid-sin-guiones",
  "Nombre Sociedad": "Empresa SL",
  "Nombre": "Ana García",
  "Email": "ana@empresa.com",
  "Consultor": "Nombre Consultor",
  "Tipo Cliente": "planes",
  "Fecha": "2026-04-17",
  "Estado": "Pendiente",
  "email_to": "ana@empresa.com",
  "email_subject": "Solicitud de creación de ficha de alta",
  "email_body": "Hola Ana, te mando este formulario...",
  "form_url": "https://.../solicitud.html?t=..."
}

Si access_token no viene, el workflow lo genera con crypto.randomUUID().

Respuesta 200

{ "id": "uuid", "success": true }
GET POST listaSolicitudes · listaRellenado · prellenarSolicitud · eliminarSolicitud X-Session-Token

Listados y utilidades del flujo de solicitudes.

PathMétodoUso
/webhook/1757fdcc-… (listaSolicitudes)GETWF08. Solicitudes NO completadas → { solicitudes:[{id,access_token,"ID Solicitud","Nombre Sociedad",Email,Estado,Fecha}] }.
/webhook/fa16b994-… (listaRellenado)GETWF09. Solicitudes con estado=completada (esperan al consultor) → { items:[{id,"Nombre Sociedad",Email,Consultor}] }.
/solicitud-prellenar-clientePOSTWF38. El consultor pre-rellena todo menos SEPA: { access_token, datos_parciales } → merge JSONB sobre solicitudes.datos respetando SEPA. El cliente solo firma.
/webhook/a2b1b1d6-… (eliminarSolicitud)POSTMismo webhook que eliminarFicha (soft-delete). { action:'delete', entity:'solicitud', id }.
POST /webhook/6da4274f-5a6d-4981-a92a-f9d7eb734144 Público

Workflow 11-auxiliares. El cliente envía sus datos desde solicitud.html. Crea la ficha, carpeta Drive, marca solicitud completada y dispara emails + tarea Asana si TPV no integrado.

Body (todos los datos del formulario)

{
  "ID Solicitud": 5,
  "access_token": "...",
  "Estado": "Rellenado cliente",
  "Nombre Sociedad": "...", "CIF/NIF": "...",
  "JP Nombre": "...", "Firmante DNI": "...",
  "TPV": "Revo",
  "TPV No Integrado": "Sí",  // si checkbox marcado
  "TPV NI Nombre": "...", "TPV NI Contacto": "...", "TPV NI Email": "...",
  "SEPA": {
    "iban": "ES91...",
    "swift": "CAIXESBB",
    "deudor": { "nombre": "...", "direccion": "...", ... },
    "tipo_pago": "recurrente",
    "firma_base64": "data:image/png;base64,...",
    "firmado_at": "2026-04-17T..."
  },
  "Contactos Extra": [   // opcional, máx 10: otras personas de la implementación (paso 03)
    { "nombre": "...", "apellidos": "...", "puesto": "...", "email": "...", "telefono": "..." }
  ]
}

Efectos

  • INSERT en fichas_alta con estado=completada + SEPA persistido.
  • Crea carpeta en Google Drive y guarda carpeta_drive en la ficha.
  • Email al cliente con enlace al Drive.
  • Email al consultor avisando.
  • UPDATE solicitudes: estado=completado + ficha_id (dispara trigger que copia fecha_solicitud).
  • Si TPV no integrado: tarea en el proyecto de Integraciones de Asana + email al equipo de soporte e integraciones.
  • Contactos Extra → lista de contactos del cliente (RPC contactos_cliente_importar): deduplica por email, no pisa ninguno existente, ignora el del jefe de proyecto y los deja con convocar = true. Al crearse el proyecto, un trigger los copia a proyectos.contactos.

Respuesta 200

{ "success": true }

💼 Ofertas y pagos

GET POST /webhook/ofertas X-Session-Token

Workflow 30-ofertas. Ofertas generadas por el configurador (departamento Consultor). GET lista con filtros; ?id=<uuid> devuelve una sola.

POST — action

actionEfecto
create · updateAlta/edición de la oferta.
archivar · reactivarEstado de archivo.
cambiar_estadoenviada (email) / ganada (ficha arrancada) / etc.
vincular_fichaAsocia la oferta a una ficha de alta.
proforma_serie lleva DOS formatos y NINGÚN consumidor debe asumir correlatividad: ofertas → PRO-YYYY-<uuid8> (preview, sin numeración fiscal); pedidos hardware (WF21) → PRO-AAAA-NNNN correlativa reservada por RPC.
GET POST oferta-publica · oferta-enviar-comercial · oferta-cambios Mixto
PathAuthUso
/oferta-publica?t=<token>Público (token)WF36. Vista pública de la oferta (configurador.html?t=).
/oferta-enviar-comercialX-Session-TokenWF37. { oferta_id, portal_base_url } → email al cliente con link, enviado desde la credencial SMTP/Gmail del consultor dueño (fallback a soporte). La ruta antigua /oferta-enviar (WF36) está en desuso: el portal ya no la llama.
/oferta-cambiosPOST público · GET HMACWF80. POST {token, snapshot} (telemetría fire-and-forget con debounce desde el configurador); GET historial por ?oferta_id=N o counts agrupados desde ofertas.html.
POST /webhook/pago/iniciar · /webhook/pago/enviar-ficha-link Público

Workflow 55-pasarela (Redsys/BBVA). El cliente, desde la oferta pública, pulsa «Pagar».

POST /pago/iniciar

// Request
{ "token": "<token-oferta>" }

// Respuesta — el front auto-submitea un form a Redsys
{
  "ok": true,
  "redirect": {
    "url": "https://sis.redsys.es/...",
    "Ds_SignatureVersion": "HMAC_SHA256_V1",
    "Ds_MerchantParameters": "<base64>",
    "Ds_Signature": "<firma>"
  }
}

El importe es final_amount × 1,21 (IVA). La firma Redsys (HMAC_SHA256_V1, 3DES en JS puro) y el inserto en payments se hacen server-side. La notificación S2S y las páginas OK/KO las sirve el propio workflow. /pago/enviar-ficha-link reenvía al email del cliente el enlace de su solicitud («Continuar más tarde»).

GET POST /webhook/escalados X-Session-Token

Workflow WF26. Ampliaciones contractuales sobre clientes existentes (módulos, locales o firmas). GET lista; POST crea o cambia el estado.

Filtros del GET, todos opcionales: ?ficha_id=<uuid> · ?tipo=modulo|local|firmas · ?estado=solicitado|pendiente|aplicado|cancelado · ?incluir_archivados=1 (los soft-deleted salen al final). Orden: pendientes primero y, dentro, más recientes primero.

actionBodyEfecto
create (por defecto){ ficha_id, tipo:'modulo'|'local'|'firmas', estado:'pendiente', detalle, setup, recurrencia, notas, creado_por }Alta en pendiente.
update{ action, id, … }Edita detalle, importes y notas.
solicitar{ action, ficha_id, solicitud_email, detalle:{ modulos[], periodicidad }, setup, recurrencia, notas, creado_por }Solo tipo:'local' (19-oct-2026). Crea el escalado en solicitado con lo comercial fijado por el staff; la BD le pone un token y se manda al cliente por correo (cuenta de Implementación) el enlace a escalado-local.html?t=. El resto lo rellena el cliente (WF-142).
reenviar{ action, id, solicitud_email? }Solo desde solicitado. Mismo token, sin fila nueva: renueva solicitud_enviada_at (la caducidad de 30 días cuenta desde ahí) y, si llega, cambia el email.
aplicar{ action, id }Solo desde pendiente. firmas: suma a fichas_alta.firmas_contratadas. modulo / local: contrata los módulos nuevos en la ficha y crea su sección en el proyecto (con el sello escalado_id). local además da de alta el local (ver abajo).
cancelar · archivar · reactivar{ action, id }Estado y soft-delete. cancelar vale desde pendiente o solicitado.

detalle según tipo

modulo: { modulos[], locales_ids[], setup, recurrencia, periodicidad:'mensual'|'anual' }
local:  { local:{ nombre, … },
          sepa:{ modo:'existente', mandato_id } | { modo:'nuevo', iban, bic, titular, sociedad_cif, sociedad_denominacion },
          modulos[], setup, recurrencia, periodicidad:'mensual'|'anual' }
firmas: { packs, firmas, setup, recurrencia }
Aplicar un local (19-oct-2026): la RPC escalado_local_aplicar() crea en una transacción la fila de locales (dirección, email, TPV y mandato), el mandato SEPA nuevo en fichas_alta.sepa_mandato si el cliente dio otra cuenta, y el encargado del local en la lista de contactos del cliente. Es idempotente por escalados.local_id. Si el TPV es integrado, pide a WF62 el ticket Zendesk + tarea Asana de integración de ese local ({ ficha_id, local_id }, etiqueta tpv-local-<local_id>), aunque el cliente ya tenga otros tickets de TPV. El local en el entorno Yurest del cliente sigue siendo manual.
Avisos de local nuevo: cuando un escalado de local llega a pendiente (el cliente envía el formulario o el staff lo crea directo), WF-143 manda un correo a Soporte, al implementador, al CSM y al comercial del cliente (uno por persona, idempotente por avisos_enviados_at; se dispara al responder y con un barrido cada 5 min). En el home, la alerta escalado_local_nuevo sale de GET /escalados?tipo=local (últimos 7 días desde que el cliente rellenó o se creó, no cancelados): quien lee Escalados ve todos; el comercial, el CSM y el implementador, los de sus clientes. Enlaza a escalados.html?alerta=escalado_local_nuevo.
solicitado no es venta: no suma en el KPI «Escalados» del home (cuenta el día que el cliente lo rellena) ni en la recurrencia.
detalle.periodicidad === 'anual' (6-oct-2026) ⇒ recurrencia es el importe del año (SEPA de pago único). Si no viene (escalados antiguos), es mensual. Contabilidad la pinta como «al año».

Respuesta 200

{ "success": true, "escalado": { … }, "errores": [] }

solicitar y reenviar añaden email_enviado (bool) y email_error. El escalado queda guardado aunque el correo falle; el front ofrece entonces «Copiar enlace» o «Reenviar».

POST /webhook/escalado-local/solicitud · /webhook/escalado-local/responder Público

Workflow WF-142. Formulario del cliente escalado-local.html?t=<token> para un escalado de local en solicitado. Sin sesión: lo autoriza el token del escalado (64 hex). Las reglas (token, estado, caducidad de 30 días desde el último envío, validación) viven en SQL: escalado_local_solicitud() y escalado_local_responder(), migración 2026-10-19_02.

PathBodyRespuesta
/escalado-local/solicitud{ t }{ found:true, estado:'solicitado', cliente, modulos[], periodicidad, setup, recurrencia, mandatos:[{ id, titular, iban, tipo_pago }], caduca_at }. El IBAN va enmascarado (ES12 •••• 1234); nunca datos de otros locales. Otros casos: { found:false } · { found:true, estado:'cancelado'|'enviada'|'caducada' }.
/escalado-local/responder{ t, datos, sol_campo_x9 }{ ok:true } y el escalado pasa a pendiente (con rellenado_at) y se disparan los avisos de WF-143. Ya enviado antes: { ok:true, ya_enviada:true }. Errores: { ok:false, error } con error ∈ token_invalido · cancelada · caducada · validacion (con campos[]) · enlace_invalido · peticion_no_valida · demasiado_grande · error_bd.

datos

{ local:    { nombre, email?, telefono?, calle, numero?, cp, municipio, provincia },
  contacto: { nombre, puesto?, email, telefono },
  tpv:      { nombre, no_integrado?, contacto?, email? },
  sepa:     { modo:'existente', mandato_id }
          | { modo:'nuevo', iban, titular, bic?, cif, denominacion, firma } }
sol_campo_x9 es el honeypot: si viene relleno se responde ok sin tocar nada. Lo comercial (módulos, importes, periodicidad) lo fija el staff al solicitar y el cliente solo lo ve.

📒 Contabilidad

POST /webhook/yurest-grabado-a3 X-Session-Token

Workflow 13-grabado-a3. Toggle del flag grabado_a3 en una ficha. Trigger SQL setea grabado_a3_at al pasar de FALSE a TRUE.

Body

{ "id": "uuid", "grabado_a3": true | false }

Respuesta 200

{
  "success": true,
  "id": "uuid",
  "grabado_a3": true,
  "affected": 1,
  "errores": []
}
GET /webhook/contabilidad/clientes-a3 X-Session-Token

Workflow 39. Lectura de Azure SQL (yurestazure / Info_PowerBI). La página pinta una tabla genérica con auto-detección de columnas; la query se ajusta en n8n (la página no envía SQL).

Respuesta 200

{ "ok": true, "total": 120, "columnas": [...], "rows": [...], "generated_at": "ISO" }
GET /webhook/contabilidad/cruce-a3 X-Session-Token

Workflow WF137 (14-oct-2026). Cruce de lo facturado en A3 con el portal, HighLevel y producto (cruce-a3.html, permiso cruce_a3). Devuelve la última foto guardada en a3_cruce_snapshot: → { ok, generado_at, corte, datos }, o { ok, datos:null } si aún no hay.

La foto no la hace n8n: A3 solo es accesible por túnel desde la oficina, así que se genera y publica a mano con scripts/a3-cruce/. El endpoint solo lee.
GET POST /webhook/facturas · /webhook/factura · /webhook/factura/borrar X-Session-Token

Workflow WF102. Facturas emitidas al cliente, que este ve en su Espacio Cliente. El PDF vive en un bucket privado: el listado nunca devuelve storage_path.

No confundir con facturas-distribuidor: son otra cosa y otra base de datos. Los nombres se parecen y no son lo mismo.
PathMétodoUso
/facturasGET→ { facturas:[…] }. Listado para facturas.html.
/facturaPOST{ ficha_id, numero, fecha_emision, concepto, importe_total, sociedad, subido_por, data } — data es el PDF en data-URL base64. Sube el objeto al bucket e inserta la fila; si el INSERT falla, el workflow compensa borrando el objeto subido.
/factura/borrarPOST{ id }.
GET POST /webhook/facturas-distribuidor[/pdf|/revisar] X-Session-Token

Workflow WF99. Facturas que los distribuidores emiten a Yurest por sus comisiones. Viven en la BD del portal de distribuidores (schema distribuidores), no en la de yurest: WF99 es el único que las toca, con la service key guardada en n8n.

PathMétodoUso
/facturas-distribuidorGET→ { facturas:[…] }. Alimenta también la alerta del home y el badge del sidebar.
/facturas-distribuidor/pdfPOST{ id } → { url }. Bucket privado: enlace firmado de 60 s, la página nunca recibe la ruta de storage.
/facturas-distribuidor/revisarPOST{ id, accion, motivo } con accion ∈ aceptar|rechazar|pagar. Al pagar responde además { comisiones_marcadas }.
Ciclo de estados: enviada → aceptada → pagada, con salida rechazada (el distribuidor corrige y sube otra del periodo). Moverlo es responsabilidad exclusiva de contabilidad; el distribuidor solo puede crear y retirar mientras está enviada.

📁 Proyectos

GET POST PUT DELETE /webhook/proyectos X-Session-Token

Workflow 01-proyectos-crud. CRUD de proyectos de implementación.

MétodoBodyRespuesta
GET—{ proyectos: [...] }
POST{ proyecto: {...} }{ data: {...}, success: true }
PUT{ proyecto: { id, ...campos } }id devuelto
DELETE{ id: "uuid" }Soft-delete

El POST/PUT acepta asanaProjectId y asanaProjectUrl para vincular un tablero Asana.

Qué trae cada proyecto en el GET

El GET devuelve los proyectos vivos (los borrados se filtran en la BD desde el 24-oct-2026) y acepta ?id=<uuid> para leer uno solo. Cada fila, completa: id, cliente, implementador, tipo (el «plan»: Basic / Pro / Corporate), estado, fecha_inicio, ultima_actividad, tpv, motivo_pausa, plan_accion, asana_project_id, asana_project_url, promocion_id, promocion_turno, ficha_id, origen_secciones, meta_updated_at y las marcas de tiempo. En JSONB: secciones, orden_secciones, anotaciones (bitácora), contactos, participantes, adjuntos y formularios.

Cada objeto de secciones puede llevar además escalado_id y escalado_fecha: el sello que pone WF26 a las secciones que nacen al aplicar un escalado. Su ausencia significa «implementación inicial». Es lo que separa las vistas «Implementación / Escalado N» del tab Proyecto; el progreso y los KPIs no cambian, las secciones de escalado siguen contando en el total.

El detalle de sesiones con sus fechas vive dentro de secciones[].tareas[]: nombre, completada, show (Show / No Show / null), fechaEntrega, tiempoEstimado, notas y subtareas[].

Contactos (5-oct-2026): contactos es una copia de la lista única de contactos del cliente (cs_contactos_cliente) que mantiene un trigger. Un PUT de proyecto ya no lo escribe y lo ignora: los contactos se cambian con /cs/accion (contacto_cliente_*). Cada tarea puede traer además bloqueada, modulo, noAplicaManual («No aplica» puesto a mano en una sesión de promoción: no se le convoca), googleEventId y eventoCompartido.
Cierre de fase por módulo (13-oct-2026): formularios.cierre_fase es una lista con un cierre por módulo (clave: el nombre de la sección, sin acentos ni mayúsculas): { seccion, fecha_cierre, periodo, sesiones[], asistentes[], criterios[6 booleanos], incidencias:[{ descripcion, impacto:'Bajo'|'Medio'|'Alto', accion }], recomendacion:{ titulo, texto }|null, decision:'avanzar'|'correctivas'|'no_avanzar', decision_texto, proximas_fases, creado_por, fecha, editado_por?, editado_at? }. Se guarda con el PUT de este endpoint (formularios completo): quien reescriba formularios debe conservar la clave, o el siguiente PUT la borra.
No vienen en el JSON dos columnas que sí se ven en el gestor: el progreso (lo calcula el navegador sobre el árbol de secciones, ver obtenerResumenProyecto en js-gestor/data.js) y cocina central (sale de la ficha del cliente, del flag es_cocina_central de sus locales).

ultima_actividad vs ultima_actividad_efectiva

La columna ultima_actividad solo la escriben este workflow (alta y PUT) y WF02 al guardar o mover una tarea. No la tocan el panel de sesiones (WF52, donde se marcan Show / No Show), el espejo de Asana (WF81) ni el importador de tareas de promoción (WF83), así que se queda vieja en cuanto las sesiones se marcan fuera del gestor.

Por eso el GET añade ultima_actividad_efectiva: el máximo entre esa columna y la fecha de la última sesión realmente dada (tarea o subtarea completada o marcada Show / No Show, con fechaEntrega en pasado; agendar a futuro no cuenta). Se calculan sobre las mismas secciones que el progreso: fuera las administrativas, Hardware, «Módulos terminados de implementar» y las marcadas descontratado.

Para «proyectos parados» usa ultima_actividad_efectiva. La columna cruda se sigue devolviendo intacta y no cambia de comportamiento. Banco de pruebas: tests/wf01-ultima-actividad.test.js.

Token de solo lectura (consultas automatizadas)

El GET de este endpoint —y solo el GET— acepta además un token de solo lectura, pensado para informes que corren sin que nadie tenga la sesión abierta en el navegador. Va en la misma cabecera que la sesión de staff:

curl -H "X-Session-Token: ro|informe-semanal|1788000000000.<hmac>" \
     {BASE_URL}/proyectos

Se firma con un secreto distinto del de las sesiones. Ese es justo el mecanismo de seguridad: para los demás webhooks el token es una firma inválida, así que devuelven 401 sin que haya hecho falta cambiar nada en ellos. Un token de lectura filtrado no abre escritura ni ningún otro endpoint.

Se pide aAdministración de sistemas. Se emite con scripts/mint-token-lectura.js.
CaducidadLa que se fije al emitirlo (365 días por defecto). Va dentro del token; al caducar, el endpoint responde 401.
RevocaciónNo es individual: se rota el secreto de lectura y caducan todos a la vez. Las sesiones de staff no se ven afectadas.
AlcanceGET /webhook/proyectos con cualquier token de lectura. GET de fichas (WF04, /webhook/018f3362-…) y GET /cs (Cascada CS, WF-122) solo con los tokens del conector de Claude (etiqueta mcp-portal): devuelven la ficha completa o el detalle CS y el conector los filtra antes de responder. Igual GET integraciones-tpv/estado (WF87), carga-datos/estado (WF-98) e incidencias-dev/estado (WF85): solo sesión de staff o mcp-portal. carga-datos/ficha y /comentarios siguen siendo solo de sesión. Cualquier otro método o ruta → 401.
El token es una credencial: da acceso de lectura a la cartera completa de proyectos, con sus contactos y su bitácora. Trátalo como una contraseña — canal privado, nunca en un correo en claro ni commiteado en un script.
Detalle del diseño y operativa (emitir, desplegar, rotar): n8n-auth-gate/README.md. Banco de pruebas: tests/token-lectura.test.js.
Conector de Claude (MCP). El staff consulta proyectos y fichas desde claude.ai a través de un Worker de Cloudflare (mcp-portal/): OAuth con el login del portal (auth/login), permiso api_claude comprobado en vivo con auth/verify y lectura con un token ro|mcp-portal… que nunca sale del Worker. Herramientas: buscar_proyectos, ver_proyecto, buscar_fichas, ver_ficha, cascada_cs, ver_cliente_cs. Lista blanca de campos: sin IBAN, mandatos SEPA, credenciales ni DNI. Banco: tests/mcp-portal.test.mjs.
PUT /webhook/proyectos/tarea X-Session-Token

Workflow 02-proyectos-tareas. Actualiza una tarea/sección dentro de un proyecto (toggle completada, edición, etc.).

Body

{
  "proyectoId": "uuid",
  "seccionNombre": "Puesta en Marcha / Finalización",
  "tarea": { "id": "...", "completada": true, "_v": "versión que se cargó", ... },
  "comprobarVersion": true
}

Respuesta 200

{ "success": true, "version": "2026-10-24T17:46:13.544670Z" }

Respuesta 409 (otra persona guardó la tarea después)

{ "success": false, "code": "conflict", "error": "Otro usuario guardó esta tarea…", "version": "versión vigente" }
Versión por tarea (24-oct-2026, migración 2026-10-24_01): el RPC sella cada tarea con _v. Con comprobarVersion:true (lo manda el gestor) rechaza con 409 (SQLSTATE PT409) la tarea que no lleva la versión vigente. Quien no lo pide (WF52, WF77, WF99) guarda sin comprobar, pero su guardado sí cambia la versión, así que una pestaña vieja del gestor ya no lo pisa. El gestor encola los guardados de una misma tarea y toma la versión nueva de cada respuesta.
Cuando el body crea una sección nueva, el workflow persiste por el RPC proyecto_upsert_tarea, único camino que no reescribe secciones entero. Acepta p_escalado_id / p_escalado_fecha opcionales y los aplica solo en la rama que crea la sección, para que el sello de escalado sobreviva (migración 2026-09-05_02_upsert_tarea_sello_escalado.sql). Sin esa migración el sello se perdía en silencio.
Guardar una tarea la reemplaza entera (no hace merge de campos). Un proceso que escriba desde una copia vieja del proyecto (una pestaña abierta hace horas) borra lo que otro canal guardó después, como el cierre de una sesión desde el panel de sesiones. Los procesos de fondo deben releer el proyecto (GET /proyectos) justo antes de escribir; así lo hace el auto-resumen de sesiones desde el 13-oct-2026.
PUT /webhook/proyectos/tarea/mover · /webhook/proyectos/tarea/eliminar X-Session-Token

Mover una tarea entre secciones (drag & drop kanban) o eliminarla.

Body — mover

{ "proyectoId": "uuid", "tareaId": "...", "seccionOrigen": "...", "seccionDestino": "..." }
GET POST /webhook/proyectos/mensajes[-pendientes] · /webhook/proyectos/historial X-Session-Token

Hilo cliente ↔ staff e historial del proyecto (WF01).

PathMétodoUso
/proyectos/mensajesGET ?proyectoId=→ { ok, mensajes:[…] } (asc). El mismo hilo que ve el cliente en espacio-cliente.html.
/proyectos/mensajesPOST{ proyectoId, texto } (máx 4000). El backend fija autor_tipo='staff' y el nombre a partir del token.
/proyectos/mensajes-pendientesGETAgregado que alimenta la alerta mensaje_cliente_pendiente del home: una fila por proyecto cuyo hilo termina en mensaje del cliente — { proyecto_id, nombre, implementador, pendientes, desde }. No devuelve el texto de los mensajes.
/proyectos/historialGETTimeline de acciones del proyecto.
POST PUT proyectos/{jefe-proyecto, preferencia-turno, onboarding-reactivar, adjuntos, anotaciones, carga-datos/marcar} X-Session-Token

Acciones puntuales sobre un proyecto que no reescriben el proyecto entero.

PathMétodoBody → Respuesta
/proyectos/jefe-proyectoPOSTWF01. { proyectoId, jp:{ nombre, apellidos, rol, mail, telefono } } → { ok, ficha_id, jp }. Solo toca fichas_alta.jp_*.
/proyectos/preferencia-turnoPOSTWF01. { proyectoId, preferencia:'manana'|'tarde'|'indiferente'|null } → { ok, ficha_id, preferencia_turno }.
/proyectos/onboarding-reactivarPOSTWF01. { proyectoId } → { ok, ficha_id, estado } o { ok:false, error }. Saca de la pausa por no-shows un onboarding grupal.
/proyectos/adjuntosPOSTWF01. { accion:'subir', proyecto_id, nombre, data } → { ok, adjunto } · { accion:'ver', proyecto_id, adjunto_id } → { ok, url } (firmada, 60 s) · { accion:'borrar', proyecto_id, adjunto_id } → { ok }. El binario vive en un bucket privado; el proyecto solo guarda { id, nombre, tipo, size, fecha, storage_path }.
/proyectos/anotacionesPUTWF02 → RPC proyecto_anotaciones_op (24-oct-2026). { proyectoId, op:'crear'|'editar'|'borrar', entrada }: aplica ese cambio de UNA entrada sobre la bitácora del servidor, bajo bloqueo, y devuelve { success, anotaciones } completa. editar conserva autor y fecha de creación; editar una entrada ya borrada → 409. La forma antigua { proyectoId, anotaciones:[…] } se fusiona (gana el cliente por id, se conservan las del servidor que no trae). La bitácora ya no viaja en el PUT de /proyectos.
/proyectos/carga-datos/marcarPOSTWF-127. { ficha_id } → { ok, estado:'en_curso', marcada_at } o { ok, duplicado:true, marcada_at }. El implementador marca «Datos cargados»: crea la tarea de revisión para el equipo de carga de datos en Asana y marca la ficha. Errores: 400 ficha_invalida · 403 sin_permiso · 404 no_ficha · 409 sin_carpeta · 502 asana (no se marca nada; se puede reintentar).
GET POST /webhook/proyectos/comunicaciones · /webhook/proyectos/comunicaciones/reenviar X-Session-Token

Pestaña «Comunicaciones automáticas» de Proyectos y de la Cascada CS: lo que el portal ha mandado al cliente (tabla comunicaciones_cliente).

PathMétodoUso
/proyectos/comunicacionesGET ?proyecto_id= o ?ficha_id= (exactamente uno)WF-128. → { ok, comunicaciones:[{ id, canal:'email'|'calendar'|'drive'|'zendesk', tipo, destinatarios[], asunto, origen, estado, detalle, ref, enviado_at, aproximado, … }], generado_at }. En fichas individuales, las invitaciones, reprogramaciones y cancelaciones de Calendar salen de las tareas del proyecto con googleEventId, sea cual sea la sección (14-oct-2026); el «Reenviar convocatoria» de una sola sesión llega con tipo:'convocatoria_individual_sesion'.
/proyectos/comunicaciones/reenviarPOSTWF-134. { comunicacion_id, destinatarios:[email…] } (1-10). Busca el correo original en los Enviados de los buzones del portal y lo reenvía tal cual (mismo HTML y adjuntos) desde el buzón de implementación. → { ok:true, enviado_a, asunto }.
/proyectos/comunicaciones/reenviarPOST modo:'ver'WF-134 (16-oct-2026). { comunicacion_id, modo:'ver' }. Misma búsqueda, pero no envía nada: devuelve el original leído → { ok:true, asunto, de, para, cc, fecha, html|null, texto|null, adjuntos:[{ nombre, tipo, bytes }] }. Las imágenes cid: van incrustadas en html como data: (hasta 3 MB). Permiso: admin, o lectura en proyectos o cs_kanban.
Los «no» de negocio del reenvío llegan con 200 y { ok:false, code } (sin_permiso, no_existe, canal_no_reenviable, no_se_envio, sin_destino_original, no_encontrado, ambiguo, gmail_lectura, gmail_envio), porque un 403 cerraría la sesión. Petición mal formada → 400. Calendar, Drive y Zendesk no se reenvían. Permiso para reenviar: admin, o escritura en proyectos o cs_kanban; para ver basta lectura. El portal pinta el html solo dentro de un iframe sandbox sin scripts.
GET POST /webhook/menciones X-Session-Token

WF-139. Menciones «@usuario» en las anotaciones (bitácora) de Proyectos y de la Cascada CS, y su aviso en el panel de alertas del escritorio. La regla vive en SQL (menciones_accion, tabla menciones, migración 2026-10-15_02): la bitácora es la misma en proyectos y cs_kanban, así que se puede mencionar a quien entra (cualquier nivel) en cualquiera de las dos, y a los comerciales (usuarios activos con rol Consultor) aunque no entren en ninguna (16 y 20-oct-2026); admin entra en todas. Quien escribe sí necesita acceso a la página. La mención se guarda con la página por la que entra el mencionado. La entrada guarda menciones:[{ id, nombre }] en proyectos.anotaciones.

PeticiónMétodoUso
?accion=usuarios&pagina=proyectos|cs_kanbanGETA quién puedo mencionar: usuarios activos con acceso a la página, sin uno mismo. → { ok, usuarios:[{ id, nombre }] }. Quien pregunta debe tener acceso a la página.
?accion=bitacora&proyecto_id=GETLectura de la bitácora para quien fue mencionado en ese proyecto (el comercial no tiene permiso de la página). → { ok, proyecto:{ id, cliente, ficha_id, anotaciones } }; sin mención en el proyecto → { ok:false, code:'sin_permiso' }. Solo lectura.
?accion=miasGETMis menciones sin leer (máx. 200), solo de páginas a las que sigo teniendo acceso (el comercial las ve todas; cada una lleva acceso para que el front abra la página o la lectura en el home). → { ok, menciones:[{ id, pagina, proyecto_id, ficha_id, cliente, entrada_id, autor, extracto, created_at }] }.
{ accion:'entrada', pagina, proyecto_id, entrada_id, extracto, usuarios:[uuid…] }POSTDeja las menciones de esa entrada como la lista (máx. 30): alta de las nuevas, baja de las quitadas; [] = entrada borrada. Las que siguen no vuelven a avisar (solo se actualiza el extracto, 280 caracteres). → { ok, creadas:[uuid], rechazadas:[uuid] } (rechazadas = sin acceso, inactivo o uno mismo).
{ accion:'leer', proyecto_id } · { ids:[…] } · { todas:true }POSTMarca leídas mis menciones (nunca las de otro). Lo hace el front al abrir las anotaciones de un proyecto. → { ok, leidas }.
Los «no» de negocio llegan con 200 y { ok:false, code } (sin_sesion, sin_permiso, pagina_no_valida, proyecto_no_existe, entrada_de_otro_proyecto, peticion_no_valida, error_bd), porque un 403 cerraría la sesión. Petición mal formada (acción que no toca a ese método, ids que no son uuid…) → 400 con code. Sin token → 401.
GET POST /webhook/portal-accesos X-Session-Token

Workflow WF-101. Accesos multiusuario al Espacio Cliente de un proyecto. Solo staff.

MétodoEntradaSalida
GET?proyectoId=<uuid>{ ok, accesos:[…] }
POST{ proyectoId, nombre, email }{ ok, acceso, enlace } — alta y enlace de alta de contraseña
POST{ accesoId, op:'reenviar' }{ ok, enlace }
POST{ accesoId, op:'revocar' }{ ok }
El GET nunca devuelve pass_hash ni set_token: el enlace solo viaja en la respuesta de la acción que acaba de generarlo.
GET POST proyectos/asana-import · asana/project-sections · asana/tasks · asana/task/stories · importador/buscar-* X-Session-Token

Proxy e importadores de Asana (las credenciales viven solo en n8n).

PathUso
/asana/tasks?projectId= · /asana/task/stories?taskId=WF07. Proxy de lectura: tareas de un proyecto e historias (comentarios) de una tarea.
/proyectos/asana-importWF81. Importa proyectos arrancados en Asana. POST { op:'tasks'|'detalles' }.
/asana/project-sectionsEn desuso: el portal no lo llama desde el 8-jul-2026. Listaba las secciones de un proyecto (pestaña Formación grupal).
/importador/buscar-drive · /importador/buscar-asanaWF88. Buscadores del importador de clientes unificado. POST { q } → { ok, items }: Sheets «Ficha Cliente» ({ title, url }) y proyectos de Asana ({ name, gid, url }).

🎓 Promociones

GET POST promociones · promociones/asana[/ocupar] · promociones/asana-import-tareas X-Session-Token

Tandas de implementación (Customer Success): 16 plazas (8 mañana + 8 tarde). Doble proyecto Asana por promoción.

PathUso
/promocionesVista con la ocupación ya calculada. La creación es manual.
/promociones/asana?project=<gid>WF56. Lee nombre + secciones en vivo → { ok, gid, nombre, total, ocupados, libres }.
/promociones/asana/ocuparEn desuso: el portal no lo llama desde el 16-sep-2026 (la promoción la asigna el implementador desde Proyectos). WF63. { project_gid, title } → renombra la 1ª sección libre → { ok, section_gid?, nuevo_titulo?, libres_restantes? }.
/promociones/asana-import-tareasRetirado. WF83 importaba las tareas del Asana de la promo; lo sustituye /promociones/sesiones (WF-109) y el front ya no lo llama.
POST /webhook/promociones/sesiones · /webhook/promociones/convocatorias X-Session-Token

Sesiones de las promociones grupales y su convocatoria en Google Calendar.

PathUso
/promociones/sesionesWF-109. { promocion_id, turno } → reescribe la sección «Sesiones de la promoción» de cada proyecto de la tanda con la plantilla fija del cronograma: fechas rotadas a su semana de arranque y las sesiones de módulos no contratados marcadas bloqueada. Idempotente: conserva lo ya impartido y el googleEventId de lo ya agendado.
/promociones/convocatoriasWF-113. { op, promocion_id, … } con op ∈ preview · convocar · reprogramar { convocatoria_id, inicio } · cancelar { convocatoria_id, motivo } · reenviar { proyecto_id, emails[1-10] } · ajustar { clave_sesion, turno:'manana'|'tarde'|'*', inicio } · fechas { promocion_ids[1-60] }. reenviar convoca como siempre y vuelve a mandar la bienvenida con su calendario a esos correos, aunque ya la tuvieran. Todo op salvo preview y fechas exige escritura en Promociones, Proyectos o Cascada CS (o admin): si no, { ok:false, error:'Sin permiso…' } (24-oct-2026; antes bastaba cualquier sesión). Lo mismo en /promociones/recordatorios y /promociones/cierre (code:'forbidden'). Se invita a todos los contactos del cliente con convocar = true; una sesión marcada «No aplica» no se convoca. Guarda en promociones_convocatorias. → { ok, creadas, actualizadas, movidas, bienvenidas:{ enviadas, fallidas }, proyectos:{ actualizados, conflictos, sinSeccion }, avisos[], omitidas[], erroresObsoletos[], … } o { ok:false, error }.
Un evento de Google Calendar por sesión, no por cliente: la sesión conjunta (misma hora en los dos turnos) es un único evento. Los ids de evento llevan el prefijo pr y WF07 rechaza tocarlos (^(dev-sim-)?pr[0-9a-f]{32}$), así que la tarea con eventoCompartido no se reagenda ni se cancela por separado.
Ajustar y fechas (17-oct-2026): ajustar fija a mano la fecha/hora (Madrid, YYYY-MM-DDTHH:mm) de una sesión del cronograma de una promoción que no ha arrancado, tenga o no clientes; va por sesión y turno, no por convocatoria. Se guarda en su fila con inicio_manual y manda sobre la plantilla: con clientes se convoca ya en esa fecha, sin ellos los que entren después se convocan en ella. Si la sesión ya está en Google hay que usar reprogramar; no admite pasado ni festivos. fechas es solo lectura para el Calendario de promociones: → { ok, op:'fechas', sesiones:[{ promocion_id, clave_sesion, turno, inicio, convocada, manual }] } (sin las canceladas).
Cancelar = «No aplica» (14-oct-2026): cancelar borra el evento y además deja esa sesión como «No aplica» (manual y bloqueada) en los proyectos invitados que siguen en la promoción, igual que el botón del gestor. No toca a los que salieron de la promo ni tareas con asistencia marcada o completadas.
Emails inválidos (13-oct-2026): un solo invitado mal escrito hace que Google rechace el evento entero. Los correos que no pasan la validación (punto antes de la @, comas, texto suelto; los caracteres invisibles pegados se limpian antes) no se invitan y salen en avisos como { tipo:'email_invalido', email, … } para corregirlos. erroresObsoletos: ids de convocatorias que arrastraban el error de un intento anterior pero ya están bien; al convocar se limpia su ultimo_error.
Evento borrado a mano en Google (13-oct-2026): en preview, convocar y reenviar se comprueba que cada convocatoria futura sigue viva en Calendar. Si se borró, la fila llega con borrada_google: true, no se reconvoca sola (va a omitidas con motivo:'borrada_google') y hay un aviso borrada_google: reprogramar a la misma fecha la recupera y cancelar la da de baja.
Desde el 17-sep-2026 se convoca al asignar la promoción: WF69 y WF58 llaman a WF-113 por su entrada interna (Execute Workflow, solo convocar). Aplica a las promociones que arrancan a partir del 2-nov-2026; las anteriores siguen por HighLevel. Los recordatorios (WF-114, /promociones/recordatorios) tienen el cron apagado.

📅 Sesiones y calendario

GET POST /webhook/calendar/event · /webhook/calendar/disponibilidad X-Session-Token

Workflow 07-calendar-asana. Google Calendar del implementador.

PathUso
/calendar/event (POST)Crea/actualiza un evento (sesión de implementación). { summary, start, end, attendees[] }. El reagendado actualiza por googleEventId.
/calendar/disponibilidadLee el calendario en un rango para detectar solapes al agendar.
GET POST PUT /webhook/panel/* Authorization: Bearer

Panel de sesiones de implementación (panel-sesiones.html; workflows WF52 y WF53 para los dashboards). Ojo: estas rutas validan el token de sesión en Authorization: Bearer <token>, no en X-Session-Token. Las rutas con id usan query param (/api/tasks/<gid> → /panel/tasks?gid=).

PathUso
GET /panel/projects?key= · /panel/tasks?gid= · /panel/notes?docId=Proyectos de Asana del implementador, sus tareas y las notas de Gemini de una sesión.
GET /panel/recent-sessions?person=[&desde=&hasta=]Sesiones recientes. Con desde/hasta (ISO, máx. 35 días; 20-oct-2026) trae esa ventana: una semana del mes o el mes entero; sin ellos, la ventana de siempre según la persona. Cada una trae resumenEnviado / resumenEnviadoAt (✅ resumen enviado) y resumenGuardado / resumenGuardadoAt (💾 guardado sin email). Los invitados van por email.
POST /panel/summarize · /panel/sesion-resumenResumen IA. sesion-resumen: { proyectoId, seccionNombre, tareaId, implementador, cliente, fecha, titulo, googleEventId } → { encontrada, show, duracionMin, resumen }.
PUT /panel/asana-task[?gid=]Guardar: { implementador, destino:'asana'|'portal', proyectoId?, tareaId?, resumen, youtubeUrl, asistencia, duracion, fecha, titulo, resumenCliente, sesionId }. Sin gid solo escribe en el proyecto del portal. Deja la sesión marcada como guardada.
POST /panel/enviarEmail real por Gmail: { implementador, correosInvitados, correosOculta, titulo, html, asunto?, sesionId }. Deja la sesión marcada como enviada.
POST /panel/drive-find-video · /drive-share-emails · /drive-share-multiple · /drive-unshare · GET /panel/cliente-recordings?gid=Grabaciones de Drive: localizar, compartir con invitados y retirar el acceso.
GET /panel/dashboard-general?implementador= · /panel/cliente?gid=&desde=&hasta= · /panel/proyectos-impl?key=WF53 (24-oct-2026: lee el portal, ya no Asana; con Auth Gate, antes era público). implementador/key = clave del catálogo (carlos, mario, hugo, javier); gid = id del proyecto en el portal. Lógica en database/n8n-workflows/53-panel-dashboards.portal.js. proyectos-impl → [{ gid, name }] (activos y pausados, por nombre comercial). cliente-recordings acepta además nombre.
GET POST /webhook/staff/privadas · /webhook/staff/privadas/reservar X-Session-Token

Workflow WF-99. Sesiones privadas agendadas por nosotros (clientes.html → Formación). Mismo motor de huecos que el Espacio Cliente, pero con sesión de staff y sin veto por cuota: aquí la cuota se enseña y decide quien agenda.

PathMétodoUso
/staff/privadasGET ?proyecto_id=→ { ok, … } con los huecos libres en la agenda del implementador del proyecto y la cuota consumida.
/staff/privadas/reservarPOST{ proyecto_id, inicio } → reserva el hueco y crea el evento.
POST /webhook/agenda-privadas/estado · /webhook/agenda-privadas/reservar Público

Workflow WF-138 (15-oct-2026). El cliente grupal agenda sus dos sesiones privadas de la promoción («Sesión Privada de Seguimiento», 30 min, y «Sesión Privada», 1 h) con el implementador de su promoción, desde agendar-sesion.html?t=<token> (enlace de los correos de la promoción). Sin sesión: lo autoriza el token del proyecto (tabla agenda_privadas_tokens). Qué está pendiente, la caducidad y la escritura en el proyecto viven en SQL (agenda_privadas_estado() / agenda_privadas_reservar(), migración 2026-10-15_02).

PathBodyRespuesta
/agenda-privadas/estado{ t }{ ok, sesiones:[…], huecos:{ <clave>:{ dias } } }: cada privada pendiente, agendada o hecha y, para las pendientes, los huecos libres del implementador (≥ 48 h, L–V 9–18, sin festivos nacionales, hasta el plazo del cierre).
/agenda-privadas/reservar{ t, clave, inicio }{ ok, sesion }: crea el evento de Google y escribe la fecha en la tarea de la plantilla del proyecto. Errores { ok:false, error } con error ∈ enlace_invalido · caducada · no_aplica · sesion_invalida · ya_agendada · antelacion · fuera_de_plazo · sin_evento · calendario_no_disponible · calendario_rechazo.
Al reservar va primero Google (si falla no se toca nada) y luego la RPC con el proyecto bloqueado; si la RPC dice que no (doble clic, otra pestaña), se borra el evento recién creado. Los correos de la promoción leen aviso_agendar (columna calculada de PostgREST en proyectos y fichas) para pintar «Tienes pendiente agendar…».
GET POST /webhook/sesiones/participacion · /webhook/sesiones/asistencia X-Session-Token

Workflow WF-100. Participación en sesiones por promoción (participacion.html).

PathMétodoUso
/sesiones/participacionGETAgrega en vivo desde sesiones: asistencia por promoción y turno.
/sesiones/participacion?solo=reengancheGETSolo el re enganche: clientes que han pasado a otra promoción, con la inicial (promocion_inicial_id), las intermedias, la actual, el número de cambios y la fecha del último. Sale de fichas_historial (acción promocion_cambiada, registrada desde el 17-sep-2026). Un cambio de turno aparece en el recorrido pero no cuenta como cambio. Lo usan la pestaña Re enganche de participacion.html y la Cascada CS (carga en segundo plano con skipAuthLogout).
/sesiones/asistenciaPOST{ marcas } — marcado en bloque. Escribe la marca en el árbol del proyecto y el trigger la proyecta de vuelta. Respuesta con { detalle } si alguna marca falla.
POST /webhook/onboarding/noshow/preview · /webhook/onboarding/noshow X-Session-Token

El implementador marca Show / No Show a mano. «Guardar y enviar» pide la vista previa del correo (WF-130) y, si se confirma, WF96 lo envía; «Guardar» a secas no llama aquí. Los dos reciben el mismo body.

Body

{ "ficha_id": "uuid", "tarea_id": "...", "motivo": "..." | null, "nota": "..." | null,
  "recupera": { "fecha": "YYYY-MM-DD", "hora": "HH:mm" | null } }   // opcional: se recupera en otra sesión ya agendada

Respuesta de /preview (no envía ni escribe nada)

{ "ok": true,
  "correos": [{ "destino": "jp@cliente.es", "cc": "...", "asunto": "...", "html": "..." }],
  "otros":   [{ "tipo": "noshow_3_pausa" } | { "tipo": "reasignacion_promo", "fecha_destino": "YYYY-MM-DD", "turno_destino": "manana|tarde" }],
  "motivo":  "ya_enviado | sin_ausencias | sin_ficha" }   // solo cuando no hay correo
Tercera ausencia: si hay un grupo con plazas en su turno, el cliente pasa a ese grupo y el correo sale cuando el cambio está hecho; si no lo hay, se pausa su onboarding. La vista previa usa el mismo motor que el envío, así que enseña exactamente el correo que saldrá.
POST /webhook/bienvenida/huecos · /webhook/bienvenida/reservar · /webhook/proyectos/bienvenida-estado Mixto

Sesión de bienvenida grupal con la CSM, reservada por el cliente desde solicitud.html o bienvenida.html?t=. Un evento de Google por hueco. Cupos y antelación (36 h) viven en SQL; un cron cada 15 min revisa rechazos y cancelaciones en Google, manda el recordatorio de 24 h y el correo de no-show, y reenvía el Meet si cambia.

PathAuthUso
/bienvenida/huecosPúblico token de solicitudWF-124. { t } → { ok, huecos:[{ inicio, fin, libres }], reserva }.
/bienvenida/reservarPúblico token de solicitudWF-124. { t, inicio, email, nombre, cliente, aparte? } → { ok, reserva, meet, calendario_ok } (reserva o reagenda; calendario_ok:false = la reserva se guardó pero Google falló). Error: { ok:false, error } con error ∈ completo | antelacion | hueco_invalido | enlace_invalido | email_invalido. aparte (64 hex) es el permiso de un solo uso que da bienvenida-estado para una sesión aparte: con él se salta huecos, antelación de 36 h y cupo, y se revalida la hora (errores aparte_invalido y los de la sesión aparte, abajo).
/proyectos/bienvenida-estadoX-Session-TokenWF01. { proyectoId, estado, inicio? } con estado ∈ reservada | asistio | no_show | cancelada | enlace | aparte | enviar_enlace → { ok, reserva }. cancelada: el equipo la cancela y el cliente recibe «elige otra fecha». enlace: página de reserva para que el equipo elija hueco por el cliente (→ { ok, enlace }). aparte (13-oct-2026) con inicio (ISO): la CSM da al cliente una hora fuera de los huecos → { ok, aparte, t, inicio, email, nombre, cliente }; el portal reserva después en /bienvenida/reservar con ese aparte (permiso de 15 min, un solo uso). enviar_enlace (20-oct-2026): encola en bienvenida_enlaces el correo «Reserva tu sesión de bienvenida» al JP de la ficha (lo manda el cron de WF-124; si la ficha grupal no tiene solicitud, crea la puente) → { ok, email, enlace, pedido_at }. Errores: aun_no_ha_sido, sin_reserva, sin_enlace, ya_asistio, ya_reservada, sin_email, estado_invalido.
Sesión aparte: lunes a viernes, entre las 08:00 y las 20:00 de Madrid con la sesión entera dentro, en pasos de 15 min, con al menos 1 h de antelación y como mucho a 60 días, sin pisar un hueco grupal (lunes, martes y viernes a las 10:30 y a las 16:00) ni otra sesión reservada. Códigos: fecha_invalida | antelacion_aparte | demasiado_lejos | dia_aparte | hora_aparte | choca_hueco | hora_ocupada. Tiene su propio evento de Google y no ocupa plaza de ningún hueco ni del cupo semanal.
Revisión del cron: si alguien mueve a mano en Google el evento de un hueco, se devuelve a la hora del hueco (Google avisa a los invitados) y se avisa a la CSM de que para cambiar fechas está «Reagendar» (13-oct-2026). Una reserva que pasa a cancelada sin motivo fuera de un reagendado se trata como cancelada por el equipo: el cliente recibe «elige otra fecha» (7-oct-2026).

🗂️ Implementación

GET POST /webhook/carga-datos/{estado,ficha,comentarios,rebuild,confirmar-email} X-Session-Token

Workflows WF-97 (rebuild) y WF-98 (lectura). Estado de la carga de datos de los clientes. Mismo par que los snapshots de Soporte: un cron reconstruye, el GET sirve lo último guardado.

PathMétodoUso
/carga-datos/estadoGETÚltimo snapshot (cron 06:15). Solo lleva los pendientes.
/carga-datos/rebuildPOSTWF-97. Reconstruye el snapshot consultando Asana y Drive.
/carga-datos/fichaGET ?ficha_id=<uuid>Estado de un cliente, en vivo (clientes.html → Ver → Consultor). No sale del snapshot: lee la ficha y, si hay tarea, pregunta a Asana. → { ok, estado, carpeta, carpetaUrl, marcadaAt, marcadaPor, completadaAt, gid, asanaUrl, onboardingEstado }.
/carga-datos/comentariosGET ?ficha_ids=<uuid>,… (máx. 100)WF-98 (15-oct-2026). Comentarios de la tarea de carga de datos en Asana de varias fichas a la vez (Proyectos › Grupales: pop-up del último y modal con todos). → { ok, fichas:{ <ficha_id>:{ gid, asanaUrl, total, comentarios:[{ autor, texto, at }], error? } } }, el más reciente primero; solo vienen las fichas con tarea.
/carga-datos/confirmar-emailPOST PúblicoWF-116. { f: <ficha_id>, s: <firma 64 hex> } desde documentos-subidos.html, el botón del correo de la carpeta. Sin sesión: lo protege la firma del enlace. → { ok, duplicado? } o { ok:false, code } con code ∈ enlace_invalido | sin_carpeta | no_ficha; 502 asana (no se marca nada; se puede reintentar) · 502 guardar + gid (la tarea ya existe y la ficha no quedó marcada: la ejecución queda en error para ponerlo a mano; no reintentar).
estado ∈ sin_carpeta | pendiente | en_curso | hecha | desconocido — el mismo vocabulario que la pestaña Documentos del Espacio Cliente (WF77).
GET /webhook/informe-seguimiento X-Session-Token

Workflow WF-104. Informe semanal de seguimiento de implementaciones. Calcula en servidor lo mismo que manda el correo de los lunes a Operaciones, así que la pantalla (informe-seguimiento.html) y el correo no pueden divergir. Sin parámetros.

El correo lo dispara el cron de los lunes a las 08:00; el GET sirve el mismo cálculo bajo demanda. La forma exacta de la respuesta la consume js/lib/informe-seguimiento.js.
POST /webhook/checklists/ia · /webhook/checklists/yurest X-Session-Token

Workflow «112 - Checklists (IA + API Yurest)». Backend del Configurador de checklists (checklists.html, Implementación → Configuradores).

PathUso
/checklists/ia{ prompt } → gpt-4o (JSON) → { ok, texto } o { ok:false, error }. La clave de la IA no sale del servidor.
/checklists/yurest{ entorno:'prod'|'dev', token, recurso:'roles'|'stores'|'products'|'providers'|'formats' } (products, providers y formats: Configurador de recetas; formats lee la tabla formatos_compra de la BD de producto) → proxy acotado a la API de Yurest (bases fijas, sin SSRF). Existe porque cliente.yurest.com/ws no manda CORS.
Responde siempre 200 con { ok, status }, también cuando Yurest devuelve 403: un 403 leído por apiFetch cerraría la sesión del portal.
POST /webhook/recetas/ia X-Session-Token

Workflow «140 - Configurador recetas (IA)» (lvfFVRT3q8Vgn134). IA del Configurador de recetas (configurador-recetas.html, Implementación → Configuradores), que llegó como artifact de Claude: sustituye a window.claude.use('sample'). Credencial «Anthropic API (bot ayuda)».

CampoUso
promptTexto del configurador (≤ 60 000 caracteres).
imagesOpcional: [{ media_type:'image/jpeg', data:<base64> }], hasta 10 fotos de la carta (el navegador las baja a JPEG de 1568 px).
tier'default' → claude-sonnet-5-5 (leer la carta) · 'quick' → claude-haiku-5-5 (ingredientes de un elaborado, IVA, familias).
Responde siempre 200 (salvo sesión no válida: 401) con { ok:true, texto, cortado } o { ok:false, code }; code usa los códigos del runtime de Claude (rate_limited, prompt_too_large, image_rejected, upstream_error…).
GET POST /webhook/recetas/historial X-Session-Token

WF-145 «Recetas Historial». Copias en el servidor del trabajo del Configurador de recetas (el mismo JSON que guarda en localStorage), para rescatar o consultar las fichas de un cliente desde cualquier equipo. La regla vive en SQL (recetas_historial_accion, tabla recetas_historial, migración 2026-10-20_03): entra quien tiene el permiso configurador_recetas (o admin) y el historial es de todo el equipo. El front guarda solo al exportar, antes de «Nuevo cliente» y antes de restaurar otra copia.

PeticiónMétodoQué hace
?accion=lista[&q=texto]GETCopias (máx. 300, la más nueva primero; q busca en el cliente) sin el estado. → { ok, copias:[{ id, cliente, motivo, resumen:{ fichas, revisadas, elaborados, productos, fuente_productos }, creado, autor }] }.
?accion=ver&id=uuidGETUna copia con su estado. → { ok, copia } o { ok:false, code:'no_existe' }.
{ accion:'guardar', motivo, estado }POSTmotivo: exportar · nuevo_cliente · restaurar · manual. estado con fichas[] y products[] (máx. 8 MB). Sin fichas no guarda (code:'vacio'); un estado idéntico a otro ya guardado no se duplica. → { ok, id, duplicada }.
{ accion:'eliminar', id }POSTBorra esa copia (migración 2026-10-21_02), mismo permiso que el resto. → { ok, id } o { ok:false, code:'no_existe' }.
«No» de negocio (sin_permiso, vacio, no_existe) con 200 y { ok:false, code }; petición mal formada 400; sin sesión 401.

🖥️ Hardware

GET POST /webhook/hardware/pedidos · /webhook/hardware/stock · /webhook/hardware/tablets X-Session-Token
PathUso
/hardware/pedidosWF21. Pedidos por proyecto: ciclo solicitada → proforma → pago → lista_envío. Consumido desde proyecto.Hardware, contabilidad.Proformas y Soporte envíos. POST { action, pedido:{ id, … } } con action ∈ create, create_tablet, actualizar_items, generar_proforma, editar_proforma, guardar_proforma_pdf, adjuntar_proforma, enviar_proforma_email, adjuntar_justificante, aprobar_justificante, confirmar_pago, devolver_a_contabilidad, devolver_a_implementador, marcar_enviado, anular → { success, pedido, errores[] }. marcar_enviado y create_tablet exigen permiso de edición de Hardware. editar_proforma { id, items, proforma_iva_pct, proforma_envio, proforma_envio_zona } corrige artículos, precios, envío e IVA de una proforma proforma_generada o proforma_enviada conservando su número de serie; con justificante, pagada o anulada no casa ninguna fila (errores «0 filas»). Desde el 13-oct-2026 se usa también desde el proyecto (pestaña Hardware), no solo desde hardware.html; el PDF se regenera después con guardar_proforma_pdf.
/hardware/stockCatálogo del almacén (Soporte): stock actual, mínimo, precios y movimientos. Acciones: create, update, archivar, reactivar, movimiento. Escribir exige permiso de edición de Stock o Hardware (si no, 200 { ok:false, code:'forbidden' }); created_by lo pone el servidor con el usuario de la sesión.
/hardware/tabletsGET → { pendientes, en_curso }: fichas con necesita_tablet y aún sin pedido, y los pedidos tipo='tablet' ya en curso. Fuente distinta al listado de pedidos — aquí manda la ficha de alta, no hardware_pedidos.
/seur/envios (POST)WF42. Envíos y recogidas con SEUR desde hardware.html. { action:'crear_envio', pedido_id, receiver, bultos, service_code, product_code, comments } · { action:'crear_recogida', pedido_id, collection_date, franja, receiver, bultos, observations } · { action:'cancelar_recogida', pedido_id, collection_ref } → { ok, shipment_code?, label_pdf?, collection_ref?, collection_date?, error? }. Exige sesión y permiso de edición de Hardware. pedido_id UUID; crear_envio solo con el pedido en lista_envio; envío y recogida reservan el pedido 90 s (un segundo intento en ese tiempo responde «Ya se está generando…»); collection_ref tiene que ser la del pedido. tracking y cancelar están retirados (25-oct-2026).

🧩 Producto

GET POST presupuestos · presupuestos-asana[-attach] · presupuestos/enviar-cliente X-Session-Token

Desarrollos a medida por cliente (reemplaza el Excel de Producto).

PathUso
/presupuestosCRUD: quién paga, estado de aprobación y de entrega.
/presupuestos-asanaGET → { tareas:[{ …, ya_importado }], total, nuevas, importadas }: tareas de la sección «Pendiente de presupuesto» (Back Clientes). POST { action:'import'|'refresh', asana_gid } las importa.
/presupuestos-asana-attachAdjunta el PDF generado a la tarea Asana de origen (base64 → multipart).
/presupuestos/enviar-clienteWF22. Envía el PDF por email al cliente desde el buzón de soporte.
GET POST /webhook/bolsa-horas · /bolsa-horas-* · /bolsa-horas/asignacion* X-Session-Token

Workflow WF66. Horas contratadas vs consumidas por cliente. Las contratadas se registran como compras (cantidad + PDF pago + PDF factura); las consumidas se derivan de los presupuestos aceptados/pagados.

PathUso
GET /bolsa-horas→ { compras:[…], asignaciones:[…] }.
/bolsa-horas-crearSube ambos PDFs e inserta la compra.
/bolsa-horas-editar · -archivar · -reemplazar-pdfEditar horas/fecha, archivar, reemplazar un PDF.
/bolsa-horas/asignacion · -borrarUpsert / borra una asignación bolsa↔presupuesto.
GET POST /webhook/triaje/asana[/edit|/orden] X-Session-Token

Workflow WF57. Lee en vivo el proyecto Asana «Sugerencias optimizaciones clientes» (tareas + secciones + etiquetas + campos, normalizado y paginado).

PathUso
GET /triaje/asanaSugerencias normalizadas.
/triaje/asana/editEdita una sugerencia → escribe en Asana (PUT name/notes + add/removeTag de módulo).
/triaje/asana/ordenOrden de prioridad manual compartido (mapa módulo→[gids], Data Table triaje_orden).
GET POST /webhook/sprint-triaje/asana[/edit|/mover] · /webhook/sprint-triaje/vacaciones X-Session-Token

Workflows WF-106 (Asana) y WF-107 (vacaciones). El tablero mensual «Sprint #N - <nombre>» de Producto, leído y escrito en vivo contra Asana. Lo usa triaje-sprint.html.

PathMétodoUso
/sprint-triaje/asanaGET [?project=<gid>]Resuelve el sprint activo (el que cubre hoy entre los «Sprint #N - nombre»; las plantillas peladas tipo «Sprint #44» quedan fuera) y devuelve tareas, secciones, miembros, catálogo de campos por gid y las notas del proyecto, de donde js/lib/sprint-triaje.js saca la capacidad de cada persona.
/sprint-triaje/asana/editPOST{ gid, assignee?, custom_fields:{ <gid>: valor } } — escribe la tarea en Asana.
/sprint-triaje/asana/moverPOST{ gid, seccion } — cambia la tarea de columna.
/sprint-triaje/productivasPOST{ dev, pct } — fija el % de horas productivas de una persona; pct: null vuelve al de su seniority (70 % senior, 50 % junior). Se guarda en la Data Table sprint_productivas y el GET de /sprint-triaje/asana lo devuelve en productivas: { <nombre>: pct }. La capacidad se calcula sobre las horas disponibles, no sobre las notas.
/sprint-triaje/vacacionesGET ?desde=YYYY-MM-DD&hasta=YYYY-MM-DDWF-107. Vacaciones del equipo de desarrollo leídas del WS API de Yurest (GET /v2/hr/absences, type_id=2, estados 1 accepted y 2 pending; fuera canceladas y borradas). → { ok, desde, hasta, tramos:[{ user_id, nombre, desde, hasta, estado }] }.
Solo devuelve al equipo del sprint, no a toda la empresa. El campo de vacaciones del triaje dejó de ser manual: el cruce con los días hábiles del sprint lo hace vacacionesPorDev() en js/lib/sprint-triaje.js.
GET POST /webhook/sprint-horas/dias · /webhook/sprint-horas/sync X-Session-Token

Workflow WF-110. Productividad por desarrollador (productividad.html, permiso triaje_sprint).

PathMétodoUso
/sprint-horas/diasGET ?project=<gid>Entradas de tiempo del sprint, día a día. Es la única fuente con granularidad diaria: actual_time_minutes es un total por tarea, sin fecha.
/sprint-horas/syncPOST{ project } — fuerza el refresco desde Asana (el cron ya lo hace cada 2 h).
La vista cuenta «hasta hoy» sin incluir el día en curso.
GET POST /webhook/errores/estado · /webhook/errores/rebuild X-Session-Token

Workflow WF-111. Informe de errores de sprint (errores.html, permiso triaje_sprint).

PathMétodoUso
/errores/estadoGETCorpus global de errores de Asana (Tipo ∈ Error, ERROR - P1, Problema configuración): todo lo abierto, tenga la edad que tenga, más lo resuelto de los últimos 6 meses.
/errores/rebuildPOSTFuerza el refresco (~30 s). El cron lo hace cada madrugada a las 06:30.
Va contra un snapshot diario porque la Search API de Asana no pagina y corta en 100: el corpus se trocea en ~57 ventanas por fecha, que no caben en los 60 s del proxy. «Resuelto» = la tarea pasó a QC (historial de columnas), no la fecha de finalización, que se pone de golpe al cerrar el sprint.
GET POST /webhook/tareas-asana/{estado,sync,detalle,guardar,comentar} X-Session-Token

Workflow WF-115. Vista Tareas Asana de Producto (tareas-asana.html, permiso tareas_asana). Proyecto Back Clientes, solo tareas abiertas de las secciones Pendiente de revisión, Pendiente de presupuesto, Necesita más info y Aprobadas.

PathMétodoUso
/tareas-asana/estadoGETSnapshot (tareas_asana_snapshot) con las tareas y la definición de sus campos. El cron lo refresca cada 15 min.
/tareas-asana/syncPOST{} — fuerza el refresco.
/tareas-asana/detalleGET ?gid=En vivo: notas, campos, comentarios, subtareas y adjuntos de una tarea.
/tareas-asana/guardarPOST{ gid, notas?, campos?: { <campo_gid>: valor | opcion_gid | null } } — escribe en Asana y actualiza la tarea en el snapshot. Las duraciones (Estimated/Actual time) son de solo lectura.
/tareas-asana/comentarPOST{ gid, texto } — crea el comentario en Asana.
Las escrituras salen con la credencial de Asana del workflow, no con la del usuario. El comentario lleva el prefijo «Nombre (portal): » con el nombre sacado del token de sesión (nunca del body), y el detalle lo quita al pintarlo.
GET POST /webhook/documento-ia[/analizar|/generar|/borrar|/preguntar|/editar] X-Session-Token

Base de conocimiento de sprints (gpt-4o). Se pega la URL de un proyecto Asana, se elige un campo enum y sus valores, y se genera un documento markdown de las funcionalidades del sprint.

PathUso
POST /documento-ia/analizar{ asana_url } → { proyecto, campos[], tareas_completadas }.
POST /documento-ia/generar{ asana_gid, sprint_nombre, campo_gid, campo_nombre, valores[] } → { documento }.
GET /documento-ia→ { documentos:[…] } (vivos). /borrar = soft delete; /editar.
POST /documento-ia/preguntar{ pregunta } → { respuesta(markdown), sprints_citados[] }.
GET POST /webhook/newsletter[/analizar|/vincular|/cargar|/guardar|/generar|/enviar-prueba|/borrar|/asignar-tipo|/combinar|/marcar-enviada] X-Session-Token

Workflow WF73. Newsletter de sprint (/updates). Analiza un proyecto Asana, reescribe títulos con IA y arma el email.

PathUso
/newsletter/analizar · /vincularDetecta proyecto/sección/campos; vincula la newsletter.
GET /newsletter · /cargar · /guardarLista / carga (merge en front) / guarda { titulo, intro, items }.
/newsletter/generar{ id, task_gids[] } → { items:[{task_gid, titulo_reescrito, beneficio}] }.
/newsletter/enviar-prueba · /borrar · /asignar-tipoEnvío de prueba, soft delete, escribe el campo «Tipo» en Asana.
/newsletter/combinar{ id, extra_id, quitar? } → añade (o quita) otro sprint a la newsletter id. Al añadir importa el borrador del otro; /cargar trae las tareas de todos.
/newsletter/marcar-enviada{ id, enviada, usuario } → enviada_at + enviada_por. Marca manual: el envío masivo aún no vive en el portal, y estado no sirve porque lo pisa el envío de prueba (enviado_prueba).

🤝 Customer Success

GET POST /webhook/cs · /webhook/cs/accion X-Session-Token

Workflow WF-118. Backend de la Cascada CS (clientes.html → Customer), los paneles cs-inicio · cs-actividad · cs-kpis y la pestaña Contactos del gestor. El workflow solo valida la sesión y llama a funciones SQL (cs_ficha, cs_resumen, cs_agenda, cs_actividad, cs_kpis, cs_tareas_impl, cs_accion): la lógica vive en la base de datos. El autor de cada acción sale del token, nunca del body.

GET /cs?recurso=…

recursoParámetrosUso
fichaficha_id (uuid)Todo lo de CS de un cliente: reuniones (hitos), contactos, follow-ups, oportunidades, incidencias, riesgo y contactos_cliente.
resumen—Cartera por fase.
agendacsm? · desde? (YYYY-MM-DD, hoy) · dias? (1-31, 7)Reuniones de la CSM en la ventana.
actividadcsm? · canal? (llamada|whatsapp|correo|reunion|otro) · solo_sin?Seguimiento de clientes.
kpismes? (YYYY-MM) · csm?Panel de KPIs Customer.
tareas_implficha_id o implementador · pendientes=1Tareas de CS a Implementación (pestaña Customer y alerta del home).

POST /cs/accion

{ "accion": "contacto_cliente_crear",
  "datos": { "ficha_id": "uuid", "nombre": "Ana", "apellidos": "", "rol": "Gerente", "email": "ana@cliente.es", "telefono": "", "convocar": true } }
acciondatos / efecto
hito_crear · hito_actualizar · hito_cancelar · hito_eliminarReuniones CS: { ficha_id, tipo, fecha_prevista } · { id, updated_at, estado, … } · { id, updated_at, motivo } · { id, updated_at }. Cancelar deja la reunión «por reagendar», cancela el evento de Google y pide al cliente que elija otra fecha. Eliminar (7-oct-2026) es un borrado lógico sin correo al cliente: la reunión desaparece de la Cascada, el evento de Google se cancela en la siguiente pasada del cron y queda apunte en cs_estado_historial → { ok, hito }. No se puede eliminar una reunión ya realizada ni la bienvenida individual (409 transicion).
contacto_crear · followup_crear · followup_actualizarRegistro de contactos con el cliente y de seguimientos.
oportunidad_crear · oportunidad_actualizar · incidencia_crear · incidencia_actualizarOportunidades e incidencias CS.
handover_guardar · drive_vincular · formulario_guardarHandover, carpeta de Drive y formularios (post 1 mes, QBR).
riesgo_marcar · riesgo_quitar · csm_asignarMarca manual de riesgo (con motivo) y CSM del cliente.
tarea_impl_crear · tarea_impl_actualizarTareas a Implementación. Permiso propio: el implementador las cierra sin escritura en CS.
contacto_cliente_crear · contacto_cliente_actualizar · contacto_cliente_borrarLista única de contactos del cliente (cs_contactos_cliente), compartida por CS e Implementación: { ficha_id, nombre, apellidos?, rol?, email, telefono?, convocar? } · { id, …campos } · { id }. → { ok, contacto_cliente, contactos }; 409 duplicado si el email ya está. contacto_cliente_actualizar { id, hacer_jp: true } hace al contacto jefe de proyecto: copia sus datos en fichas_alta.jp_*, el JP anterior entra en la lista y responde además { jp, jp_anterior }; 400 si el contacto no tiene email.

Respuesta

{ "ok": true, ... }
{ "ok": false, "code": "validacion | sin_usuario | sin_permiso | no_encontrado | conflicto | transicion | duplicado | accion_desconocida", "error": "texto legible" }
El status HTTP es el que devuelve la función SQL (400, 401, 403, 404, 409). proyectos.contactos es una copia de esta lista que mantiene un trigger (ver proyectos).
GET POST /webhook/highlevel/* · /webhook/highlevel/prod-sin-crm X-Session-Token

Integración HighLevel (CRM). Credenciales (LOCATION_ID + TOKEN) solo en n8n.

PathUso
/highlevel/contactosWF40. Listado de contactos. Filtros server-side: ?limit(1-500), ?cursor("ts,id"), ?q, ?tag, ?origen.
/highlevel/contacto-notas · /tagsNotas del CRM de un contacto bajo demanda (GET ?contactId); catálogo de tags del location. Si el contacto ya no existe en HighLevel (borrado o fusionado) → 404 { ok:false, code:'contacto_hl_no_encontrado' }: reintentar no sirve, hay que corregir el contacto vinculado en la ficha o en su oferta.
POST|PUT /highlevel/contacto-notasCrea ({contactId, texto}) o edita ({contactId, noteId, texto}) una nota del contacto. El autor sale del token de sesión, cruzado por email con los usuarios de HighLevel; si no existe allí, la firma va al final del texto. Sin borrado. La usa la pestaña Anotaciones del proyecto.
/highlevel/usuariosUsuarios de HighLevel {usuarios:[{id, nombre}]}, para mostrar el autor de las notas.
/highlevel/clientes-ganadosWF49. Snapshot de oportunidades ganadas (3 pipelines) enriquecidas con contacto/business. Formato { ok, total, columnas, rows, generated_at }. Desde el 7-oct-2026 el snapshot guarda también los clientes Lite (los necesita Estado clientes) y es este GET el que los filtra.
/highlevel/clientes/refreshWF50. Refresca el snapshot on-demand (cron 6:30 UTC). Si HighLevel limita peticiones (429) y fallan más de max(5, 2 %) de las lecturas, aborta antes de borrar: se conserva el snapshot anterior.
POST /highlevel/cs/refreshWF-136 (7-oct-2026). Refresca highlevel_cs_snapshot: oportunidades de los pipelines de Customer Success (onboarding Planes y Lite, Corporate y Enterprise; sin Churn ni Upsell). Responde 202 { ok:true, lanzado:true } y sigue por detrás; cron diario 6:45. Misma regla de aborto que WF50. Es la fuente principal del tipo de implementación de Estado clientes.
/highlevel/prod-sin-crmWF51. Clientes vivos en producto SIN oportunidad ganada (match prod_id↔id_bbdd, fallback CIF).
/highlevel/oportunidades-abiertas · /contacto-detalleWF67. Picker de oferta desde oportunidad abierta.
GET POST /webhook/clientes-free[/toggle|/refresh|/asana-task] X-Session-Token

Workflow WF76. Clientes Free 3 meses. GET lista clientes_free_3m; /toggle persiste Free(0)/Lite(1); /refresh re-sincroniza contra HighLevel sin esperar al cron de las 6.

GET /webhook/home/kpis-hoy X-Session-Token

Resumen del día para el dashboard del home. GET sin parámetros → { fecha, ofertas, fichas, bajas, escalados } (contadores incrementales de home_kpis_diarios, mantenidos por triggers AFTER INSERT).

Los contadores salen de aquí; el panel de alertas del home no. Salvo mensaje_cliente_pendiente (que sí tiene endpoint, /proyectos/mensajes-pendientes) , factura_distribuidor_pendiente y escalado_local_nuevo (lee /escalados?tipo=local), las alertas se calculan en el navegador sobre las fichas ya cargadas (lista.js) y solo hacen deep-link a lista.html con el filtro puesto.
Vocabulario invertido en fichas_alta.estado: completada = la rellenó el cliente y falta que la complete el consultor; rellenado = ya la completó el consultor (estado terminal). Por eso la alerta ficha_pendiente_introducir filtra por rellenado sin introducido_yurest, y solo desde el 22-jun-2026, fecha en que nació esa columna: sin el corte entraría todo el histórico, que la tiene a false por defecto.
GET /webhook/home/contadores X-Session-Token

Workflow WF-117. Contadores de los badges del menú calculados en el servidor, para que el home no tenga que bajarse /altas completo (3,6 MB → 161 B). Sin parámetros.

{
  "version": 1,
  "a3": 12,
  "sinasignar_ids": ["…"],
  "sinasignar_sin_id": 0,
  "proformas": 3,
  "hardware": { "lista_envio": 2, "solicitada": 1 }
}
sinasignar_ids va como lista de ids y no como número porque el navegador descuenta los borrados recientes que guarda en localStorage. Las tablets pendientes y las facturas de distribuidor no salen de aquí.
Las reglas viven dos veces: en el workflow (generado desde database/n8n-workflows/117-home-contadores.build.mjs) y en js/lib/contadores-home.js, copia exacta de _contarXCliente de config.js, que es el respaldo si WF-117 falla. tests/contadores-home.test.js compara ambas: cambiar una regla obliga a cambiar las dos y redesplegar el workflow.

🛟 Soporte

GET zendesk/tickets-heatmap[-ia] · zendesk/resumen-semanal · zendesk/resumen-mensual X-Session-Token
PathUso
/zendesk/tickets-heatmapMapa de calor: ?from=YYYY-MM-DD&to=YYYY-MM-DD → matriz [día 0-6][hora 0-23].
/zendesk/tickets-heatmap-iaIgual pero solo tickets aún en el agente IA (asunto «Conversation with»): cobertura del bot.
/zendesk/resumen-semanalResumen ejecutivo IA de la semana. ?week=actual|anterior o ?from&to.
/zendesk/resumen-mensualWF29. Análisis profundo (gpt-4o) con cache por (año, mes). ?refresh=1.
GET /webhook/informe-mensual-soporte X-Session-Token

Workflow WF105. Informe mensual de KPIs de soporte: una fila por mes en informes_mensuales_soporte. El cron del día 1 lanza las 52 consultas a la Search API de Zendesk del mes anterior y guarda el informe ya redactado. Lo consume informe-mensual-soporte.html.

ParámetroUso
?month=YYYY-MMMes del informe. Por defecto, el mes anterior.
?refresh=1Regenera. Responde 202 y sigue por detrás.
Sin refresh no toca Zendesk: sirve lo guardado. Con refresh=1 la generación pasa del minuto y el proxy corta a los 60 s, así que la página sondea después sin refresh hasta ver un generado_en nuevo — no reintentes el refresh en bucle.
GET POST zendesk/orgs · cliente/zendesk-link · zendesk/tickets-cliente · registro-error* X-Session-Token

Asociación cliente ↔ organización Zendesk (modal «Ver cliente» → Tickets) y registro de errores.

PathUso
/zendesk/orgsWF41. Lista organizaciones con filtro server-side (nombre/external_id/details/notes).
/cliente/zendesk-linkWF42. PATCH zendesk_org_id (o NULL). 409 si la org ya está asociada a otro cliente.
/zendesk/tickets-clienteWF43. Tickets de la org asociada (ventana default 180d).
/registro-error/org-users?org_id= · /registro-errorCrea ticket Zendesk + tarea Asana enlazados. GET org-users → { users:[…] } (miembros de la organización). POST { org_id, requester_id, modulo, asunto, descripcion, cliente, proyecto_id? | ficha_id?, origen?, miembro_nombre?, imagenes:[{ nombre, tipo, base64 }] } → { ok, ticket_id, ticket_url, asana_gid, asana_url, imagenes_subidas }. Con origen:'cascada_cs' (Soporte técnico de la Cascada CS, 6-oct-2026) el ticket lleva la etiqueta cascada_cs en vez de gestor_proyectos.
GET POST /webhook/zendesk/ticket/recordatorios · /webhook/zendesk/ticket/nota-interna X-Session-Token

Workflow WF07. Actividad de un ticket Zendesk desde la tarjeta de integraciones.html.

PathMétodoUso
/zendesk/ticket/recordatoriosGET ?ticketId=→ { ok, ticketId, ultimoComentario:{ texto, fecha, autor, publico } | null, recordatorios:[{ fecha, regla, email, estado }] }, lo más reciente primero. Los recordatorios salen de los audits del ticket (automatizaciones): no son comentarios y /comments no los ve.
/zendesk/ticket/nota-internaPOST { ticketId, texto }Añade una nota interna (19-oct-2026). El autor sale del token de sesión, no del body («Nota de <nombre>…»); si su email es agente de Zendesk, la nota sale además a su nombre. → { ok, ticketId, comentario:{ texto, fecha, autor, publico:false } } o 400 { ok:false, error } (texto vacío, ticket cerrado o inexistente…).
GET POST incidencias-dev/{estado,rebuild} · integraciones-tpv/{estado,rebuild} X-Session-Token

Snapshots diarios (06:00) + refresco manual. Cruce Zendesk × Asana.

PathUso
GET /incidencias-dev/estadoÚltimo snapshot de incidencias DEV.
POST /incidencias-dev/rebuildReconstruye el snapshot.
/integraciones-tpv/estado · /rebuildGemelo para integraciones TPV (filtro por proveedor).
GET POST /webhook/cliente/asana-formacion-link · /webhook/asana/formacion-tasks X-Session-Token

Pestaña Formación · proyecto Asana asociado al cliente.

En desuso: el portal no llama a estas dos rutas desde el 8-jul-2026 (la pestaña Formación muestra el proyecto de implementación).
PathUso
/cliente/asana-formacion-linkWF44. { cliente_id, asana_url } → extrae GID, verifica el proyecto y PATCH asana_formacion_gid. null desvincula.
/asana/formacion-tasks?project_gid=WF45. → { project, counts:{done,pending,total}, tasks:[…] }.
GET POST /webhook/9e0fc21e-… · /2c62e049-… · /6c6b655b-… · /buscar-resumen · /042f57e0-… X-Session-Token

Mini-app «Churn técnico» (iframe). Webhooks autenticados por el Auth Gate HMAC: churnClientes, churnResumenGenerar, churnNiveles, churnResumenBuscar, churnTickets.

GET POST PUT /webhook/notif-integraciones/{config,grupos,historial} X-Session-Token

Notificaciones automáticas de Integraciones: configuración, grupos de destinatarios e historial de envíos.

PathMétodoUso
/notif-integraciones/configGET · PUTConfiguración. PUT { activo, umbral_dias, secciones_seguimiento[], updated_by }.
/notif-integraciones/gruposPOSTAlta, edición y borrado de grupos de destinatarios.
/notif-integraciones/historialGETHistorial de envíos.

📦 Producto Yurest (snapshot prod)

GET POST /webhook/prod/clientes[/refresh|/nombres] X-Session-Token

Uso del producto Yurest cacheado en Supabase (prod_clientes_snapshot) para evitar la query pesada al MySQL de producción en cada apertura.

PathUso
GET /prod/clientesWF47. Lee el snapshot → { ok, total, refreshed_at, clientes:[…], hl }. Cada cliente trae, entre otros, cantidad_locales e integraciones (texto separado por comas, Sin integraciones si no hay), de donde Estado clientes calcula en el navegador la tipología, los KPIs y el filtro por integración. hl: { ok, oportunidades, cs } son las oportunidades ganadas de HighLevel (WF50) y las de los pipelines de CS (WF-136): con ellas se pinta la columna Implementación (Grupal / Individual). Si hl.ok no es true, la columna queda sin dato.
POST /prod/clientes/refreshWF48. Reejecuta la query MySQL (cron 6am UTC). Tarda 5-15s → { ok, count, refreshed_at }.
GET /prod/clientes/nombresLista ligera id+nombre+nombre_comercial de TODA la tabla (para casar nombre HL → id cliente).

⚙️ Administración

GET POST /webhook/admin/passwords · /webhook/admin/passwords/reveal X-Session-Token

Workflow WF91. «Mis contraseñas»: gestor de credenciales de portales externos. El servidor resuelve userId y rol del token; un no-admin solo ve/edita las suyas, el admin todas.

PathUso
GET /admin/passwordsLista propia (o ?scope=all admin). Nunca devuelve la contraseña, solo metadatos.
POST /admin/passwords{ action:'save'|'delete', entry:{…} }. La contraseña se cifra en BD (pgcrypto) vía RPC; la clave la pone el workflow, nunca viaja al cliente.
POST /admin/passwords/reveal{ id } → plano (dueño|admin) y audita en admin_passwords_audit.
GET POST /webhook/admin/equipo X-Session-Token

Workflow WF92. «Mi equipo»: inventario por persona. Jerarquía tipo(sección) → catálogo(modelo, coste solo-admin) → unidad(física asignada a un usuario).

MétodoUso
GET ?what=tipos|catalogo|unidades[&scope=all]unidades: propias sin coste; admin+scope=all → todas + coste + inventario económico agregado.
POST{ what:'tipo'|'catalogo'|'unidad', action:'save'|'delete', payload:{…} }. Escrituras solo admin.
GET POST /webhook/historial · /webhook/historial/global X-Session-Token

Audit log.

PathUso
/historialHistorial de acciones por ficha.
/historial/globalFeed unificado (usuarios + fichas + proyectos) para Logs de Administración (solo admin). Query: ?limit&offset&fuente&desde&hasta&q → { historial:[…], hasMore }. Vista v_historial_global. q busca en el actor, la descripción y, desde el 13-oct-2026, también en el id y el tipo de la entidad (pegar el id de una ficha, un proyecto o un usuario).

👥 RRHH

GET POST /webhook/cocina/estado · /webhook/cocina X-Session-Token

Tareas de cocina (tareas-cocina.html, autoservicio).

PathMétodoUso
/cocina/estado?desde=&hasta=GET→ { hoy, dia_activo, gestionar, personas, turnos, tareas, checks }. Nunca devuelve los PIN.
/cocinaPOST{ accion:'marcar', fecha, tarea, hecho, pin } · { accion:'intercambiar', fecha_a, fecha_b, pin } · { accion:'gestion', op, payload } (admin o escritura rrhh).
El PIN solo lo compara la base de datos; cinco fallos bloquean a la persona 15 minutos.
GET POST /webhook/rrhh/documentos X-Session-Token

Gestión documental (documentos-rrhh.html): NDA y cesión de equipos en PDF por empleado. Quién ve qué lo decide la base de datos: el gestor ve a todos; el resto, solo lo suyo.

MétodoBodyRespuesta
GET—{ gestionar, yo, empleados:[{ id, nombre, documentos:{ nda?, cesion_equipos? } }] }
POST{ accion:'subir', usuario_id, tipo, nombre, data } · { accion:'borrar', id }Admin o escritura rrhh.
POST{ accion:'ver', id }{ url } firmada, 60 s.
POST{ accion:'cesion_preparar' } · { accion:'cesion_firmar', equipo_hash, dni, … } · { accion:'cesion_detalle', id }El empleado revisa y firma su declaración de cesión de equipos.

📉 Bajas

GET POST /webhook/84f094b2... · /webhook/73ce8d34... · /webhook/95d5ed5d... X-Session-Token

Workflow 05-bajas. Gestión de bajas de clientes (registro, edición de campos individuales y borrado).

EndpointOperación
bajasGET listado
bajaCliente · bajaLocal · bajaModulos · bajaEditarPOST campos específicos (mismo webhook con action)
bajaBorrarPOST borra una baja
GET POST /webhook/bonificaciones X-Session-Token

Workflow WF112 («Bonificaciones CRUD»). Bonificaciones de MRR de la vista Churn (churn.html, permiso bajas).

MétodoEntradaSalida
GET—{ ok, bonificaciones:[…] }
POST{ accion:'crear'|'editar'|'borrar', id?, cliente, periodo:'AAAA-MM', … }{ ok, bonificacion } o { ok:false, error }

📊 Distribución

POST /webhook/6d3ed726-c86a-4b86-a2ae-7f07da9630a5 X-Session-Token

Workflow 06-distribucion. Asigna un implementador a una ficha.

En desuso: el portal no lo llama desde el 13-jul-2026 (distribucion.html es un panel de solo lectura).

Body

{ "fichaId": "uuid", "implementador": "Nombre Implementador" }
GET POST /webhook/distribuidor/detectados · /webhook/distribuidor/detectados-estado X-Session-Token

Workflow WF78. Distribuidores potenciales: cuando el consultor rellena el TPV de un local y su email de contacto, el workflow saca el dominio y, si no consta como distribuidor conocido, avisa al responsable de distribuidores y registra la fila.

PathUso
GET /distribuidor/detectados→ { ok, rows:[{ dominio, email_origen, contacto_nombre, tpv_nombre, ficha_id, cliente_nombre, local_nombre, estado, avisado_at }] }.
POST /distribuidor/detectados-estado{ dominio, estado } (nuevo|contactado|descartado|alta) → PATCH de la fila.

📁 Drive

GET /webhook/2010bb2b-... · /webhook/bdef8517-... X-Session-Token

Workflow 11-auxiliares. getDrive devuelve la URL de la carpeta Drive de una ficha; docsSubidos consulta si hay documentos subidos.

En desuso: el portal no llama a docsSubidos desde el 24-abr-2026. getDrive sigue en uso.

Query

?id=<ficha_uuid>

🏠 Espacio Cliente

API que consume espacio-cliente.html (servido en espacio-cliente.yurest.es). No comparte base ni token con el resto de esta documentación.

Base distinta: {BASE_URL}/portal-cliente
Cabecera distinta: X-Cliente-Token (no X-Session-Token)
Las rutas están declaradas públicas en el gate de n8n porque la validación la hace el propio workflow WF77. El cliente final no tiene cuenta de portal: su sesión vive en localStorage.yurest_cliente_sesion y un 401 la borra y devuelve al login.
El backend resuelve siempre el proyecto y la ficha a partir del token, nunca del body. Un ficha_id o proyecto_id que llegue en el cuerpo se ignora.
Llave maestra (staff): un acceso marcado como maestra no cuelga de ningún proyecto. Elige cliente con GET /proyectos y manda en cada petición X-Cliente-Proyecto: <proyecto_id>; WF77 solo atiende esa cabecera si el acceso es maestra. Es de solo lectura: cualquier método distinto de GET se corta en el front y también en WF77 (code: 'solo_lectura').
POST portal-cliente/{set-password, login, reset} Público

Alta de contraseña, login y recuperación. Los accesos los crea el staff desde portal-accesos (WF-101).

PathEntradaSalida
/set-password{ set_token, password }{ ok }. Valida token y caducidad, escribe hash+salt, estado='activo' y quema el token.
/login{ email, password }{ token, proyecto:{ id, nombre } } o { error } genérico.
/reset{ email }{ ok } siempre.
Los errores son deliberadamente genéricos: un acceso pendiente (aún sin contraseña) y uno bloqueado responden igual que una contraseña mala. No se le dice al atacante en qué estado está la cuenta.
GET POST portal-cliente/{proyecto, proyectos, mensajes, accion, tickets} X-Cliente-Token
PathMétodoUso
/proyectoGETPayload del espacio: nombre, implementador, secciones con sus tareas (nombre, completada, show, fechaEntrega, notas), adjuntos, contactos y el resumen de sesión publicado.
/proyectosGETSolo llave maestra. → { ok, proyectos:[{ id, nombre, razon_social, implementador, estado }] } para el selector de cliente.
/mensajesGET · POSTHilo con el staff (asc). POST { texto }, máx 4000. El mismo hilo que ve el staff en /proyectos/mensajes.
/accionPOST{ tareaId, accion, hecha } — marca una acción del resumen de sesión.
/ticketsGETTickets de Zendesk de su organización (whitelist de 6 campos; la organización sale del token).
El payload de GET /proyecto se construye campo a campo, no es un SELECT * podado. Fuera sin excepción: datos financieros, sepa_mandato, cualquier cred_*, anotaciones, motivo_pausa y plan_accion.
GET POST portal-cliente/hardware/* X-Cliente-Token

Pedidos de hardware desde el Espacio Cliente (WF77). Sin aviso a Slack: se ven en la pestaña Hardware y en la alerta del home del staff.

PathMétodoUso
/hardware/catalogoGETArtículos que el cliente puede pedir.
/hardware/pedidosGETSus pedidos y el estado de cada uno.
/hardware/pedidoPOST{ idem_key, items, direccion_entrega }. La idem_key la genera el front: reenviar el mismo pedido no duplica.
/hardware/pdfGET ?pedido_id=&tipo=→ { ok, pdf }. Proforma o factura del pedido.
/hardware/justificantePOST{ pedido_id, nombre, base64 } — el cliente sube el justificante de pago.
/hardware/proforma-pdfPOST{ pedido_id, base64 } — guarda el PDF de la proforma que genera el navegador del cliente. Responde 404 si el pedido no es suyo.
El estado inicial del pedido es solicitada (femenino, concuerda con «proforma»). Escribirlo en masculino rompe los filtros.
GET POST portal-cliente/{facturas, factura/enlace, privadas*, carga-datos/*, contrato*} X-Cliente-Token
PathMétodoUso
/facturasGETSus facturas (las que sube el staff por WF102). Sin storage_path.
/factura/enlaceGET ?id=Enlace firmado de 60 s al PDF. No copiar la firma a 1 año de WF35.
/privadasGETHuecos libres para una sesión privada y la cuota que le queda.
/privadas/reservarPOST{ inicio }. A diferencia de staff/privadas, aquí la cuota sí veta.
/privadas/cancelarPOST{ tarea_id }.
/carga-datos/estadoGETEstado de su carga de datos (mismo vocabulario que WF-98).
/carga-datos/marcarPOSTBody vacío. El cliente declara que ya ha subido sus datos → crea la tarea de revisión en Asana (idempotente por gid). Errores: 502 asana (no se marca nada; se puede reintentar) · 502 guardar + gid (tarea creada, ficha sin marcar: ejecución en error para arreglarlo a mano; el Espacio Cliente lo da por recibido).
/contratoGET→ { ok, estado, numero, firmado_at, tiene_pdf }. estado es lo que escribe efirma (FIRMADO, enviado…); el front lo pasa a minúsculas.
/contrato/pdfGET→ { ok, pdf:{ data } } en base64, o 404. El PDF se descarga de Drive en el servidor: así se abre desde el móvil sin sesión de Google y la carpeta no se expone.
Hoy ninguna ficha tiene contrato (WF84/WF86 y el Worker del PDF siguen sin activar): /contrato responde estado: null y la pestaña enseña su estado vacío.

Apéndice A · Tipos de datos

Objeto SEPA

{
  "referencia": "YUREST-5",
  "acreedor": {
    "identificador": "ES##ZZZB40597437",
    "nombre": "Yurest Solutions Sociedad Limitada",
    "direccion": "Calle Transits, 6",
    "cp": "46002", "poblacion": "Valencia", "provincia": "Valencia",
    "pais": "España"
  },
  "deudor": {
    "nombre": "...", "direccion": "...",
    "cp": "...", "poblacion": "...", "provincia": "...", "pais": "..."
  },
  "swift": "CAIXESBB",
  "iban": "ES9121000418450200051332",
  "tipo_pago": "recurrente" | "unico",
  "fecha_firma": "2026-04-17",
  "localidad": "Valencia",
  "firma_base64": "data:image/png;base64,...",
  "firmado_at": "2026-04-17T14:33:33.152Z"
}

Objeto Adjunto

{
  "id": "adj_xxx",
  "nombre": "contrato.pdf",
  "tipo": "application/pdf" | "image/png" | "image/jpeg",
  "size": 12345,
  "data": "data:application/pdf;base64,...",
  "fecha": "2026-04-17T..."
}

Objeto paquetes_carrito

{
  "items": [{ "id": "plan_pro", "precio": 1580 }],
  "descuentos": { "cart": 0, "planes_setup": 0, "planes_recur": 0 },
  "periodos": { "planes": "mensual" | "anual" },
  "corp_module_locales": { "modulo_x": [0, 1] },
  "locales_mensual": [25, 30],
  "modo": "por_local",                                   // opcional: carrito configurado local a local
  "locales_modulos": [{ "modulos": [{ "modulo": "Compras" }] }],   // con modo por_local: módulos de cada local, en el orden de los locales
  "impl_grupal": { "precio": 250, "descuento": 0 }   // implementación grupal: bruto + % de descuento; ImporteSetup guarda el neto
}

Apéndice B · Tabla de estados

fichas_alta.estado

EstadoSignificadoQuién lo asigna
pendienteInicial (poco usado)—
completadaCliente rellenó su parte (workflow 11)Cliente vía solicitud.html
rellenadoConsultor completó la ficha (workflow 04)Consultor vía index.html

solicitudes.estado

EstadoSignificado
pendienteEsperando que el cliente la rellene
completadoCliente rellenó (genera ficha)
Nota semántica: los nombres son legacy. "completada" en fichas_alta ≠ "completado" en solicitudes. Los triggers SQL ya alinean fechas con esta semántica (fecha_rellenado, fecha_completado, grabado_a3_at) para que la analítica sea coherente.

Apéndice C · Permisos de página

Cada usuario lleva permisos granulares { read, write, delete } por id de página (emitidos en el login). El rol es una base + deltas. usuarios.permisos es un JSONB sin CHECK: el catálogo vive solo en el front (PERMISOS_DISPONIBLES de config.js), así que un permiso nuevo no necesita migración y WF16 ignora los ids que no conoce. IDs disponibles:

GrupoIDs de página
Informesdistribucion · informe_implementacion · informe_seguimiento · carga_datos · participacion · informe_tickets · informe_tickets_ia · incidencias_dev · integraciones_tpv · resumen_semanal · resumen_mensual · informe_mensual_soporte · churn_tecnico
Consultorlista · escalados · configurador · ofertas · distribuidores_detectados
Implementaciónsinasignar · proyectos · panel_sesiones · promociones · importador_clientes · checklists · configurador_recetas
Contabilidadcontabilidad · clientes_a3 · cruce_a3 · proformas · facturas_cliente · facturas_distribuidores
Customer Successcs_inicio · cs_kanban · cs_actividad · cs_kpis · cs_direccion · bajas · churn · highlevel · highlevel_clientes · huerfanos_prod
Productotriaje · triaje_sprint · presupuestos · bolsa_horas · documento_interno_ia · updates · tareas_asana · errores · productividad
Soporteestado_clientes_yurest · integraciones · documentacion_integraciones · hardware · stock · clientes_free · resumen_semanal · resumen_mensual · informe_mensual_soporte
I+Dpermisos_roles · editor_etiquetas
Admin / Otrosadmin · logs · rrhh · mis_contrasenas · mi_equipo · docs
Desde el 25-sep-2026 cada página tiene su propio permiso: el que muestra el enlace en el menú y en el home es el mismo que pide la página al entrar (requireAuth). clientes queda en el catálogo sin uso (la Cascada CS pide cs_kanban). Son públicas para cualquier sesión: «Landings», «Doc. integraciones», «Mis contraseñas» y las de RRHH de autoservicio (Tareas de cocina, Mi equipo, Gestión documental). La fuente de verdad es sidebar.js (campo permiso, y si falta, el id).