{"openapi":"3.1.0","info":{"title":"Inmovilla API","description":"API pública y de administración de Inmovilla (academia, blog, cursos, leads y web comercial). Los endpoints autenticados usan Bearer API tokens (`imv_…`) gestionados desde el panel de administración. Índice de contenido para agentes: https://www.inmovilla.com/llms.txt","version":"1.0.0","contact":{"url":"https://www.inmovilla.com"}},"servers":[{"url":"https://www.inmovilla.com"}],"tags":[{"name":"Public"},{"name":"Docs","description":"Entradas de documentación de la academia. Soporte multi-idioma con traducciones."},{"name":"Doc FAQs","description":"Preguntas frecuentes asociadas a entradas de documentación."},{"name":"Courses","description":"Cursos de la academia. Soportan capítulos, tests, y multi-idioma."},{"name":"Course Chapters","description":"Capítulos/lecciones de un curso."},{"name":"Blog","description":"Posts del blog corporativo con soporte multi-idioma."},{"name":"Media files","description":"Biblioteca de archivos del equipo (imágenes comprimidas, PDF, ZIP, documentos). Hotlinks de marca en `/file/<public_id>` (302 al bucket público `media-files`)."},{"name":"OAuth 2.1 (conectores MCP)","description":"Authorization server integrado para clientes MCP (Claude Desktop, ChatGPT). Registro dinámico (RFC 7591), authorization code + PKCE S256, refresh tokens rotativos y revocación (RFC 7009). Los access tokens son tokens `imv_` normales (aparecen en /admin/api con origen OAuth). Discovery: `/.well-known/oauth-authorization-server` y `/.well-known/oauth-protected-resource/api/v1/mcp`."},{"name":"Redirects","description":"Redirecciones 301 permanentes para conservar SEO al mover contenido entre URLs públicas."},{"name":"Global Blocks","description":"Bloques globales compartidos entre páginas del landing builder. Los cambios se propagan a todas las páginas vinculadas."},{"name":"Landing Pages","description":"Páginas del web-editor. Modelo draft/published por página: cada `landing_page` tiene un `landing_page_version` en estado `draft` y otro en `published`. Las mutaciones impactan al draft; el cambio se hace público con `/publish`. Bloques se gestionan vía `/blocks` (reemplazo masivo) y `/reorder`. El payload de creación admite el JSON completo (slug + seo + theme_config + blocks[]) — mismo shape que el botón \"Importar JSON\" del editor. Catálogo live de tipos de bloque: `/api/v1/landing-blocks` (MCP: `inmovilla_list_landing_blocks` / `inmovilla_get_landing_block`)."},{"name":"Landing Pages (admin-only AI)","description":"Endpoints IA del editor web. Solo accesibles con sesión admin (cookie); no expuestos vía Bearer token."},{"name":"Categories","description":"Categorías para documentación. Soportan jerarquía padre-hijo y traducciones."},{"name":"Blog Categories","description":"Categorías específicas para el blog."},{"name":"Tags","description":"Etiquetas para documentación, cursos y blog."},{"name":"Web Editor Settings","description":"Configuración global del editor web para scripts comunes de toda la web pública."},{"name":"Promotions","description":"Promociones y banners del sitio."},{"name":"Contact","description":"Formulario de contacto público. Endpoint sin auth, rate-limited por IP (5/15min). Persiste el lead en la tabla `lead` (auditoría, siempre) y lo reenvía a los sinks activados en el bloque: webhook interno (CONTACT_FORM_WEBHOOK_URL) y/o Vixiee (CRM)."},{"name":"Demo","description":"Formulario de solicitud de demo. Endpoint público, rate-limited por IP (5/15min). Persiste el lead (auditoría) y lo reenvía a los sinks activados en el bloque: webhook interno (DEMO_FORM_WEBHOOK_URL) y/o Vixiee (CRM)."}],"paths":{"/api/v1/health":{"get":{"tags":["Public"],"summary":"Estado del servicio (público, sin autenticación).","responses":{"200":{"description":"`{ status: \"healthy\", version, timestamp }`."}},"security":[]}},"/api/v1/search":{"get":{"tags":["Public"],"summary":"Búsqueda full-text pública sobre academia, cursos y blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"q","in":"query","required":true,"description":"Texto de búsqueda","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"description":"es | en | pt | it | fr (default: es)","schema":{"type":"string"}},{"name":"type","in":"query","required":false,"description":"academy | course | blog | all (default: all)","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"Página (default: 1)","schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"description":"Resultados por página (default: 20)","schema":{"type":"number"}}],"security":[]}},"/api/v1/mcp":{"post":{"tags":["Public"],"summary":"Endpoint MCP (Model Context Protocol, Streamable HTTP). Requiere Bearer token `imv_…`.","responses":{"200":{"description":"Respuesta JSON-RPC 2.0."}},"security":[{"bearerAuth":[]}]}},"/api/v1/docs":{"get":{"tags":["Docs"],"summary":"Lista docs con paginación, filtros por categoría/tag, y búsqueda full-text.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"page","in":"query","required":false,"description":"Página (default: 1)","schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"description":"Resultados por página (default: 20, máx: 100)","schema":{"type":"number"}},{"name":"category_id","in":"query","required":false,"description":"Filtrar por categoría","schema":{"type":"string","format":"uuid"}},{"name":"tag_id","in":"query","required":false,"description":"Filtrar por tag","schema":{"type":"string","format":"uuid"}},{"name":"locale","in":"query","required":false,"description":"Filtrar por idioma","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Búsqueda full-text","schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:read`."},"post":{"tags":["Docs"],"summary":"Crea una nueva entrada de documentación.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL (kebab-case)"},"is_public":{"type":"boolean","description":"Visible públicamente"},"youtube_url":{"type":"string","description":"URL de vídeo YouTube"},"category_id":{"type":"string","format":"uuid","description":"ID de categoría principal"},"tag_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs de tags asociados"},"translations":{"type":"array","items":{"type":"object"},"description":"Array de traducciones (mín. 1)"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/docs/{id}":{"get":{"tags":["Docs"],"summary":"Obtiene un doc por ID con traducciones, categorías, tags y cursos relacionados.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:read`."},"put":{"tags":["Docs"],"summary":"Actualiza un doc existente. Todos los campos son opcionales.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."},"delete":{"tags":["Docs"],"summary":"Elimina un doc y sus traducciones, FAQs y relaciones.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/docs/tree":{"get":{"tags":["Docs"],"summary":"Obtiene el árbol de docs organizado por categoría.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:read`."}},"/api/v1/docs/search":{"get":{"tags":["Docs"],"summary":"Búsqueda full-text en docs.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"q","in":"query","required":true,"description":"Texto de búsqueda","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"description":"Filtrar por idioma","schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:read`."}},"/api/v1/docs/reorder":{"put":{"tags":["Docs"],"summary":"Reordena docs dentro de su categoría.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"doc_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs en nuevo orden"}},"required":["doc_ids"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/docs/{id}/faqs":{"get":{"tags":["Doc FAQs"],"summary":"Lista las FAQs de un doc.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:read`."},"post":{"tags":["Doc FAQs"],"summary":"Crea una nueva FAQ para un doc.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sort_order":{"type":"number","description":"Posición"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones de la FAQ"}},"required":["translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/docs/{id}/faqs/{faqId}":{"put":{"tags":["Doc FAQs"],"summary":"Actualiza una FAQ existente.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"faqId","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."},"delete":{"tags":["Doc FAQs"],"summary":"Elimina una FAQ o solo las traducciones de los locales indicados (JSON body { locales: [\"pt\"] } o ?locales=pt,en). Sin locales se elimina la FAQ completa.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"faqId","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/docs/{id}/faqs/reorder":{"put":{"tags":["Doc FAQs"],"summary":"Reordena FAQs de un doc.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"faq_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"IDs en nuevo orden"}},"required":["faq_ids"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `docs:write`."}},"/api/v1/courses":{"get":{"tags":["Courses"],"summary":"Lista cursos con paginación y filtros.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"page","in":"query","required":false,"description":"Página (default: 1)","schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"description":"Resultados por página (default: 20, máx: 100)","schema":{"type":"number"}},{"name":"status","in":"query","required":false,"description":"available | hidden | coming_soon","schema":{"type":"string"}},{"name":"level","in":"query","required":false,"description":"beginner | intermediate | advanced","schema":{"type":"string"}},{"name":"role","in":"query","required":false,"description":"agent | manager | coordinator | lister | marketing. Incluye cursos sin roles (para todos). Valor desconocido → 400","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"description":"Filtrar por idioma","schema":{"type":"string"}},{"name":"translation_scope","in":"query","required":false,"description":"Solo con locale=pt: primary (PT público) | fallback_es (ES público sin PT)","schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:read`."},"post":{"tags":["Courses"],"summary":"Crea un nuevo curso.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL"},"instructor_id":{"type":"string","format":"uuid","description":"ID del miembro del equipo (con is_instructor=true)"},"price":{"type":"number","description":"Precio (0 = gratuito)"},"level":{"type":"string","description":"beginner | intermediate | advanced"},"target_roles":{"type":"array","items":{"type":"string"},"description":"Perfiles destinatarios: agent | manager | coordinator | lister | marketing. Vacío = todos"},"status":{"type":"string","description":"available | hidden | coming_soon"},"type":{"type":"string","description":"recorded | live"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones del curso"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/courses/{id}":{"get":{"tags":["Courses"],"summary":"Obtiene un curso con capítulos, instructor asignado, categorías y tags.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:read`."},"put":{"tags":["Courses"],"summary":"Actualiza un curso existente.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."},"delete":{"tags":["Courses"],"summary":"Elimina un curso y todos sus capítulos, tests y relaciones.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/courses/reorder":{"put":{"tags":["Courses"],"summary":"Reordena cursos.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/courses/{id}/chapters":{"get":{"tags":["Course Chapters"],"summary":"Lista capítulos de un curso ordenados por sort_order.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:read`."},"post":{"tags":["Course Chapters"],"summary":"Crea un nuevo capítulo. La duración (vídeos de YouTube incrustados + lectura) se calcula al guardar y se devuelve en cada traducción (duration_seconds, video_seconds, reading_seconds, video_durations).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sort_order":{"type":"number","description":"Posición"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones (título, descripción, contenido). Opcional manual_seconds: minutos de vídeo a mano (en segundos) mientras YouTube no devuelva la duración de algún vídeo"}},"required":["translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/courses/{id}/chapters/{chapterId}":{"put":{"tags":["Course Chapters"],"summary":"Actualiza un capítulo y recalcula su duración; solo consulta YouTube para vídeos nuevos o sin resolver. También sincroniza course.duration_minutes (derivado).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."},"delete":{"tags":["Course Chapters"],"summary":"Elimina un capítulo.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/courses/{id}/chapters/reorder":{"put":{"tags":["Course Chapters"],"summary":"Reordena capítulos de un curso.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `courses:write`."}},"/api/v1/blog":{"get":{"tags":["Blog"],"summary":"Lista posts del blog con paginación.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"page","in":"query","required":false,"description":"Página (default: 1)","schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"description":"Resultados por página (default: 20, máx: 100)","schema":{"type":"number"}},{"name":"status","in":"query","required":false,"description":"draft | published | archived","schema":{"type":"string"}},{"name":"category_id","in":"query","required":false,"description":"Filtrar por categoría de blog","schema":{"type":"string","format":"uuid"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:read`."},"post":{"tags":["Blog"],"summary":"Crea un nuevo post de blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL"},"status":{"type":"string","description":"draft | published | archived"},"is_featured":{"type":"boolean","description":"Post destacado"},"featured_image":{"type":"string","description":"URL de imagen destacada"},"publish_date":{"type":"string","format":"date-time","description":"Fecha de publicación"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones del post"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:write`."}},"/api/v1/blog/{id}":{"get":{"tags":["Blog"],"summary":"Obtiene un post por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:read`."},"put":{"tags":["Blog"],"summary":"Actualiza un post existente.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:write`."},"delete":{"tags":["Blog"],"summary":"Elimina un post.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:write`."}},"/api/v1/blog/reorder":{"put":{"tags":["Blog"],"summary":"Reordena posts del blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog:write`."}},"/api/v1/media-files":{"get":{"tags":["Media files"],"summary":"Lista archivos subidos con paginación y búsqueda por nombre.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"page","in":"query","required":false,"description":"Página (default: 1)","schema":{"type":"number"}},{"name":"limit","in":"query","required":false,"description":"Resultados por página (default: 20, máx: 100)","schema":{"type":"number"}},{"name":"q","in":"query","required":false,"description":"Filtro ilike sobre original_name","schema":{"type":"string"}},{"name":"mime","in":"query","required":false,"description":"MIME exacto, o prefijo de tipo (image, video, text)","schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `media-files:read`."},"post":{"tags":["Media files"],"summary":"Sube un archivo (multipart field `file`). JPEG/PNG se convierten a WebP. Máx. 10 MiB imagen / 25 MiB otros.","responses":{"200":{"description":"201 + fila media_file (public_id, hotlink_url, public_url Storage, mime, size, dimensiones si imagen)."}},"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Archivo (multipart/form-data)"}},"required":["file"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `media-files:write`."}},"/api/v1/media-files/from-url":{"post":{"tags":["Media files"],"summary":"Descarga un archivo público por URL (servidor a servidor, con protección SSRF: solo hosts públicos, máx. 3 redirecciones, 20 s) y lo guarda como una subida normal. Pensado para agentes MCP (`inmovilla_upload_media_from_url`), que no pueden enviar binarios. Rate limit extra: 30/min por IP.","responses":{"200":{"description":"201 + fila media_file con hotlink_url y source_url (URL final tras redirecciones)."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL http(s) absoluta y pública"},"filename":{"type":"string","description":"Nombre a mostrar (por defecto, el de la URL)"}},"required":["url"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `media-files:write`."}},"/api/v1/media-files/{id}":{"get":{"tags":["Media files"],"summary":"Detalle de un archivo por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `media-files:read`."},"delete":{"tags":["Media files"],"summary":"Elimina la fila y el objeto en Storage. El hotlink `/file/:publicId` deja de funcionar.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `media-files:write`."}},"/file/{publicId}":{"get":{"tags":["Media files"],"summary":"Hotlink público (sin auth). 12 hex chars. Responde 302 a la URL de Storage o 404.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"publicId","in":"path","required":true,"schema":{"type":"string"}}],"security":[]}},"/api/v1/oauth/register":{"post":{"tags":["OAuth 2.1 (conectores MCP)"],"summary":"Registro dinámico de cliente. Solo hosts de redirección permitidos (claude.ai, claude.com, chatgpt.com, openai.com, localhost; ampliable con OAUTH_REDIRECT_HOST_ALLOWLIST). Rate limit 20/h por IP.","responses":{"200":{"description":"201 + client_id (+ client_secret si el método no es none), redirect_uris, grant_types."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"redirect_uris":{"type":"array","items":{"type":"string"},"description":"URIs https (o http en localhost), sin fragmento"},"client_name":{"type":"string","description":"Nombre mostrado en la pantalla de consentimiento"},"token_endpoint_auth_method":{"type":"string","description":"none (default) | client_secret_basic | client_secret_post"},"grant_types":{"type":"array","items":{"type":"string"},"description":"authorization_code (+ refresh_token)"},"scope":{"type":"string","description":"Scopes por defecto (resource:action separados por espacio)"}},"required":["redirect_uris"]}}}},"security":[]}},"/admin/oauth/authorize":{"get":{"tags":["OAuth 2.1 (conectores MCP)"],"summary":"Pantalla de consentimiento (authorization_endpoint). Requiere sesión de editor (redirige a /admin/login?redirect=…). Parámetros: response_type=code, client_id, redirect_uri, code_challenge (S256), state, scope?, resource?.","responses":{"200":{"description":"302 a redirect_uri con code y state (o error + state)."}},"security":[{"cookieAuth":[]}]}},"/api/v1/oauth/token":{"post":{"tags":["OAuth 2.1 (conectores MCP)"],"summary":"Emite access_token (imv_, 7 días) + refresh_token (90 días, rotativo). grant_type=authorization_code (code, redirect_uri, client_id, code_verifier) o refresh_token. Rate limit 60/min por IP.","responses":{"200":{"description":"200 + { access_token, token_type: Bearer, expires_in, refresh_token, scope }. Errores RFC 6749 { error, error_description }."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"grant_type":{"type":"string","description":"authorization_code | refresh_token"},"client_id":{"type":"string","description":"También aceptado vía Authorization: Basic"},"code":{"type":"string","description":"Código de autorización (grant authorization_code)"},"code_verifier":{"type":"string","description":"PKCE verifier (grant authorization_code)"},"redirect_uri":{"type":"string","description":"Debe coincidir con la autorización"},"refresh_token":{"type":"string","description":"grant refresh_token"}},"required":["grant_type","client_id"]}}}},"security":[]}},"/api/v1/oauth/revoke":{"post":{"tags":["OAuth 2.1 (conectores MCP)"],"summary":"Revoca un access token (imv_) o un refresh token (imvr_). Siempre 200 si la petición es válida.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Access o refresh token"}},"required":["token"]}}}},"security":[]}},"/api/v1/redirects":{"get":{"tags":["Redirects"],"summary":"Lista todas las reglas de redirección configuradas.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:read`."},"post":{"tags":["Redirects"],"summary":"Crea una nueva redirección 301.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"source_path":{"type":"string","description":"Ruta pública exacta de origen"},"destination_path":{"type":"string","description":"Ruta interna o URL externa de destino"},"is_active":{"type":"boolean","description":"Activa o desactiva la regla"}},"required":["source_path","destination_path"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:write`."}},"/api/v1/redirects/{id}":{"get":{"tags":["Redirects"],"summary":"Obtiene una redirección concreta por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:read`."},"put":{"tags":["Redirects"],"summary":"Actualiza una redirección existente.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:write`."},"delete":{"tags":["Redirects"],"summary":"Elimina una redirección.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:write`."}},"/api/v1/redirects/url-options":{"get":{"tags":["Redirects"],"summary":"Devuelve URLs internas del sistema para el selector de destino.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:read`."}},"/api/v1/redirects/import":{"post":{"tags":["Redirects"],"summary":"Importa redirecciones en bloque desde CSV (multipart file o JSON { csv }). Columnas: origin, destination, status_code (301), active.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `redirects:write`."}},"/api/v1/global-blocks":{"get":{"tags":["Global Blocks"],"summary":"Lista todos los bloques globales.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."},"post":{"tags":["Global Blocks"],"summary":"Crea un nuevo bloque global.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nombre visible del bloque global"},"type":{"type":"string","description":"Tipo de bloque (hero, navbar, footer, etc.)"},"variant":{"type":"string","description":"Variante del bloque (default si se omite)"},"props":{"type":"object","description":"Propiedades del bloque"},"style_tokens":{"type":"object","description":"Tokens de estilo"}},"required":["name","type"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/global-blocks/{id}":{"get":{"tags":["Global Blocks"],"summary":"Obtiene un bloque global por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."},"put":{"tags":["Global Blocks"],"summary":"Actualiza un bloque global. Los cambios se reflejan en todas las páginas vinculadas.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nombre visible"},"type":{"type":"string","description":"Tipo de bloque"},"variant":{"type":"string","description":"Variante"},"props":{"type":"object","description":"Propiedades"},"style_tokens":{"type":"object","description":"Tokens de estilo"}}}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."},"delete":{"tags":["Global Blocks"],"summary":"Elimina un bloque global. Las referencias se convierten en bloques locales (ON DELETE SET NULL).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-blocks":{"get":{"tags":["Landing Pages"],"summary":"Catálogo de tipos de bloque del web-editor (registry live): type, label, category, variants, docs_path. Para schema completo de un tipo: GET /api/v1/landing-blocks/:type (fields + default_props).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."}},"/api/v1/landing-blocks/{type}":{"get":{"tags":["Landing Pages"],"summary":"Detalle de un bloque: variants, fields (tipo, i18n, showWhen, sidebarTab…), default_props. Fuente de verdad para agentes MCP al componer landings (p.ej. type=demo-form).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."}},"/api/v1/landing-pages":{"get":{"tags":["Landing Pages"],"summary":"Lista todas las páginas con sus versiones (draft + published) y bloques resueltos.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"page_type","in":"query","required":false,"description":"Filtrar: site | campaign","schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."},"post":{"tags":["Landing Pages"],"summary":"Crea una página completa con su versión draft y bloques iniciales (todo en un solo POST). Acepta el shape de `createLandingPageSchema` — equivalente a 'Importar JSON' en el editor.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL (kebab-case, único)"},"page_type":{"type":"string","description":"site | campaign (default: site). Las 'campaign' se sirven bajo /landing/<slug>."},"is_homepage":{"type":"boolean","description":"Marcar como homepage (solo una por tenant)"},"available_from":{"type":"string","description":"Inicio de ventana de disponibilidad (ISO 8601 UTC) para landings de campaña. null = sin límite inferior."},"available_until":{"type":"string","description":"Fin de ventana de disponibilidad (ISO 8601 UTC). Fuera de rango, /landing/<slug> devuelve 404."},"seo":{"type":"object","description":"Mapa locale-keyed: { es: {title,description,...}, en: {...}, ... }"},"theme_config":{"type":"object","description":"Overrides de tema: { primary_color?, secondary_color?, font_heading?, ..., highlight_color?, button_bg_color?, button_text_color? }. Los colores de marca (`highlight_color`, `button_bg_color`, `button_text_color`) son hex estricto `#RRGGBB` o `\"\"` (= tema actual); cualquier otro valor devuelve 400."},"blocks":{"type":"array","items":{"type":"object"},"description":"Lista inicial de bloques del draft. Cada item: { type, variant?, preset?, props?, style_tokens?, is_visible?, visible_locales?, sort_order, global_block_id? }"}},"required":["slug"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/{id}":{"get":{"tags":["Landing Pages"],"summary":"Detalle completo de una página: `draft_version` y `published_version`, cada una con sus bloques resueltos (globales hidratados), SEO y theme_config por locale. El `draft_version` se devuelve con cookie admin o con un Bearer token `landing-pages:read`. Incluye alias planos `draft_blocks`/`published_blocks` a primer nivel para consumidores MCP.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."},"put":{"tags":["Landing Pages"],"summary":"Actualiza metadatos de la página y/o el SEO/theme del draft. Todos los campos opcionales (partial update).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Nuevo slug (kebab-case)"},"page_type":{"type":"string","description":"site | campaign"},"is_homepage":{"type":"boolean","description":"Marcar/desmarcar homepage"},"available_from":{"type":"string","description":"Ventana de campaña: inicio (ISO 8601 UTC). null limpia el extremo."},"available_until":{"type":"string","description":"Ventana de campaña: fin (ISO 8601 UTC). null limpia el extremo."},"seo":{"type":"object","description":"Mapa locale-keyed"},"theme_config":{"type":"object","description":"Overrides de tema (reemplaza el del draft). Incluye los colores de marca `highlight_color`, `button_bg_color`, `button_text_color` (hex `#RRGGBB` o `\"\"`)."}}}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."},"delete":{"tags":["Landing Pages"],"summary":"Elimina la página y todas sus versiones/bloques. No permitido sobre la homepage.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/{id}/publish":{"post":{"tags":["Landing Pages"],"summary":"Publica el draft de UNA página: copia bloques del draft a la versión `published` y estampa `published_at`. Invalida cache de la página pública.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/publish-all":{"post":{"tags":["Landing Pages"],"summary":"Publica el draft de TODAS las páginas `site` en una sola operación (las páginas sin draft se omiten). Invalida cache de cada página. Devuelve `{ published, skipped, failed }`. Es lo que ejecuta el botón principal 'Publicar' del editor.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/{id}/discard":{"post":{"tags":["Landing Pages"],"summary":"Descarta los cambios del draft. Restaura el draft desde la versión publicada (o lo deja vacío si la página nunca se publicó).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/{id}/blocks":{"put":{"tags":["Landing Pages"],"summary":"Reemplaza atómicamente TODOS los bloques del draft. Borra los anteriores e inserta los nuevos. Operación idempotente.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"blocks":{"type":"array","items":{"type":"object"},"description":"Lista ordenada completa. Cada item: { type, variant?, preset?, props?, style_tokens?, is_visible?, visible_locales?, sort_order, global_block_id? }"}},"required":["blocks"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/{id}/reorder":{"put":{"tags":["Landing Pages"],"summary":"Reordena los bloques del draft por UUID. Conserva el contenido — solo cambia `sort_order`.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"block_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Array de UUIDs en el orden deseado"}},"required":["block_ids"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/landing-pages/campaign-stats":{"get":{"tags":["Landing Pages"],"summary":"Agregados por landing de campaña (visitas + conversiones) en una sola llamada, vía el RPC `landing_campaign_stats()`. Conversión = nº de leads con `landing_page_id` = esa página. Devuelve `[{ page_id, visits, conversions }]`.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."}},"/api/v1/landing-pages/{id}/visit":{"post":{"tags":["Landing Pages"],"summary":"PÚBLICO (sin auth). Registra el volumen de una visita a una landing de campaña (sin datos de usuario: solo landing_page_id + locale + timestamp). Lo dispara el beacon del navegador. Rate-limit por IP (60/min).","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"locale":{"type":"string","description":"Locale de la visita (es|en|pt|it|fr)"}}}}}},"security":[]}},"/api/v1/landing-pages/generate":{"post":{"tags":["Landing Pages (admin-only AI)"],"summary":"Genera un payload de landing importable a partir de un brief en texto libre. Usa Claude Sonnet 4.6 (tool_use) o GPT-5 (function calling) con el JSON Schema del registry como input_schema, con fallback automático al otro proveedor si el primario falla. Post-valida con parseImportJson + findContentCompletenessIssues (reglas por bloque), auto-linka navbar/footer/cta a globales existentes, hidrata globales en el payload y opcionalmente strip-ea theme_config. Rate limit: 5/min por IP.","responses":{"200":{"description":"{ data: { payload: ImportedLandingPage, tokensUsed: number, costUsd: number, provider: \"anthropic\" | \"openai\" } } — payload listo para POST /api/v1/landing-pages. `provider` indica qué proveedor produjo la respuesta (diferente del `provider` del request si hubo fallback)."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","description":"Brief libre (20-2000 caracteres)"},"locales":{"type":"array","items":{"type":"string"},"description":"Idiomas a rellenar (subset de es/en/pt/it/fr). Default: [\"es\"]."},"provider":{"type":"string","description":"Proveedor primario. El otro se usa como fallback automático si el primario falla. Default: \"anthropic\"."},"allow_theme_override":{"type":"boolean","description":"Si true, el LLM puede emitir un theme_config custom para esta página; si false (default) el server strip-ea cualquier theme_config para mantener la paleta de marca global."}},"required":["prompt"]}}}},"security":[{"cookieAuth":[]}]}},"/api/v1/categories":{"get":{"tags":["Categories"],"summary":"Lista todas las categorías con traducciones.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:read`."},"post":{"tags":["Categories"],"summary":"Crea una nueva categoría.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL"},"parent_id":{"type":"string","format":"uuid","description":"ID de categoría padre"},"sort_order":{"type":"number","description":"Posición"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:write`."}},"/api/v1/categories/{id}":{"get":{"tags":["Categories"],"summary":"Obtiene una categoría por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:read`."},"put":{"tags":["Categories"],"summary":"Actualiza una categoría.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:write`."},"delete":{"tags":["Categories"],"summary":"Elimina una categoría.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:write`."}},"/api/v1/categories/reorder":{"put":{"tags":["Categories"],"summary":"Reordena categorías.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `categories:write`."}},"/api/v1/blog-categories":{"get":{"tags":["Blog Categories"],"summary":"Lista todas las categorías de blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog-categories:read`."},"post":{"tags":["Blog Categories"],"summary":"Crea una categoría de blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog-categories:write`."}},"/api/v1/blog-categories/{id}":{"get":{"tags":["Blog Categories"],"summary":"Obtiene una categoría de blog por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog-categories:read`."},"put":{"tags":["Blog Categories"],"summary":"Actualiza una categoría de blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog-categories:write`."},"delete":{"tags":["Blog Categories"],"summary":"Elimina una categoría de blog.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `blog-categories:write`."}},"/api/v1/tags":{"get":{"tags":["Tags"],"summary":"Lista todos los tags.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `tags:read`."},"post":{"tags":["Tags"],"summary":"Crea un nuevo tag.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","description":"Slug URL"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones"}},"required":["slug","translations"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `tags:write`."}},"/api/v1/tags/{id}":{"get":{"tags":["Tags"],"summary":"Obtiene un tag por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `tags:read`."},"put":{"tags":["Tags"],"summary":"Actualiza un tag.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `tags:write`."},"delete":{"tags":["Tags"],"summary":"Elimina un tag.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `tags:write`."}},"/api/v1/web-editor/settings":{"get":{"tags":["Web Editor Settings"],"summary":"Obtiene el head code y footer code globales.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:read`."},"put":{"tags":["Web Editor Settings"],"summary":"Actualiza el head code y footer code globales.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"head_code":{"type":"string","description":"Código al final de <head>"},"footer_code":{"type":"string","description":"Código al final de <body>"}}}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `landing-pages:write`."}},"/api/v1/promotions":{"get":{"tags":["Promotions"],"summary":"Lista todas las promociones.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `promotions:read`."},"post":{"tags":["Promotions"],"summary":"Crea una nueva promoción.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nombre interno"},"status":{"type":"string","description":"draft | active | paused | archived"},"type":{"type":"string","description":"line | banner | modal"},"start_date":{"type":"string","format":"date-time","description":"Fecha de inicio"},"end_date":{"type":"string","format":"date-time","description":"Fecha de fin"},"translations":{"type":"array","items":{"type":"object"},"description":"Traducciones"}},"required":["name"]}}}},"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `promotions:write`."}},"/api/v1/promotions/{id}":{"get":{"tags":["Promotions"],"summary":"Obtiene una promoción por ID.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `promotions:read`."},"put":{"tags":["Promotions"],"summary":"Actualiza una promoción.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `promotions:write`."},"delete":{"tags":["Promotions"],"summary":"Elimina una promoción.","responses":{"200":{"description":"Respuesta JSON estándar `{ success, data }`."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"bearerAuth":[]}],"description":"Permiso requerido: `promotions:write`."}},"/api/v1/contact":{"post":{"tags":["Contact"],"summary":"Envía un mensaje de contacto. Valida, aplica rate-limit, persiste y reenvía a los destinos activos (webhook interno y/o Vixiee). Devuelve 201 aunque algún sink falle (se loguea).","responses":{"200":{"description":"201 { data: { received: true } } en éxito. 400 VALIDATION_ERROR si el payload es inválido. 429 RATE_LIMITED si se supera el umbral por IP."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nombre (2–120)"},"email":{"type":"string","description":"Email válido (≤200)"},"phone_prefix":{"type":"string","description":"Prefijo internacional (ej. \"+34\"). Preferido con phone_number"},"phone_number":{"type":"string","description":"Número nacional (dígitos). Opcional; si se envía, mín. 6 dígitos"},"phone":{"type":"string","description":"Legacy: teléfono completo (≤40). El servidor ensambla E.164 desde split fields cuando existen"},"company":{"type":"string","description":"Empresa (≤200)"},"message":{"type":"string","description":"Mensaje (10–5000)"},"consent":{"type":"boolean","description":"Aceptación de política de privacidad (debe ser true; auto al enviar)"},"marketing_opt_in":{"type":"boolean","description":"Opt-in comunicaciones comerciales (default false)"},"locale":{"type":"string","description":"es | en | pt | it | fr"},"query_params":{"type":"object","description":"UTMs / gclid capturados de la URL"},"website":{"type":"string","description":"Honeypot — debe llegar vacío. Si trae valor, la respuesta es 201 silenciosa y no se dispara webhook."},"send_to_internal_webhook":{"type":"boolean","description":"Toggle del bloque. Reenvía al webhook interno AWS. Default true si se omite."},"send_to_vixiee":{"type":"boolean","description":"Toggle del bloque. Reenvía a Vixiee (CRM). Default false. Locale pt → pipeline PT; el resto → ES."}},"required":["name","email","message","consent"]}}}},"security":[]}},"/api/v1/demo":{"post":{"tags":["Demo"],"summary":"Envía una solicitud de demo. Valida, aplica rate-limit, persiste y reenvía a los destinos activos (webhook interno y/o Vixiee). Devuelve 201 aunque algún sink falle (se loguea).","responses":{"200":{"description":"201 { data: { received: true } } en éxito. 400 VALIDATION_ERROR si el payload es inválido. 429 RATE_LIMITED si se supera el umbral por IP."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nombre (2–120)"},"commercial_name":{"type":"string","description":"Nombre comercial de la agencia (2–200)"},"email":{"type":"string","description":"Email válido (≤200)"},"phone_prefix":{"type":"string","description":"Prefijo internacional (ej. \"+34\"). Preferido con phone_number"},"phone_number":{"type":"string","description":"Número nacional (mín. 6 dígitos) — o legacy `phone`"},"phone":{"type":"string","description":"Legacy: teléfono completo. Servidor ensambla E.164"},"used_before":{"type":"string","description":"\"yes\" | \"no\" — ¿ha usado Inmovilla antes? (default \"no\")"},"consent":{"type":"boolean","description":"Aceptación de política de privacidad (debe ser true)"},"marketing_opt_in":{"type":"boolean","description":"Opt-in comunicaciones comerciales (default false)"},"locale":{"type":"string","description":"es | en | pt | it | fr"},"query_params":{"type":"object","description":"UTMs / gclid capturados de la URL"},"website":{"type":"string","description":"Honeypot — debe llegar vacío."},"send_to_internal_webhook":{"type":"boolean","description":"Toggle del bloque. Reenvía al webhook interno AWS. Default true si se omite."},"send_to_vixiee":{"type":"boolean","description":"Toggle del bloque. Reenvía a Vixiee (CRM). Default false. Locale pt → pipeline PT; el resto → ES."}},"required":["name","commercial_name","email","phone_number","consent"]}}}},"security":[]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API token (`imv_…`) creado en el panel de administración."},"cookieAuth":{"type":"apiKey","in":"cookie","name":"session","description":"Sesión de administrador (solo panel interno)."}}}}